はじめに
- EC2上のtomcatにデプロイされたウェブアプリケーションのセッション情報をどうやって永続化するかを考えてまずはそれをまとめてみました。
- 次に、DynamoDBとAWSが提供しているTomcatSessionManagerを使ってSpringBootからDynamoDBにセッションを永続化する方法をご紹介します。
- SpringBoot内ではEmbeddedTomcatを使っており初期化処理時にSessionManagerを差し替えれば実現できます。EmbeddedではないTomcat上のウェブアプリケーションであればcontext.xmlに設定を書けば実現することができます。後者についてはあっさり触れたいと思います。
HTTPセッションの保持・維持について
- ログインして、商品情報を検索しカートに保存し、カートに保存された内容をもとに決済に進むようなショッピングサイトを例に考えてみます。
セッションの永続化について
- 1台構成の場合、サーバがダウンして再起動した場合にはTomcatが保持しているメモリは消えますのでログインしたあとに操作した内容(カートの情報や検索履歴など)は消えてしまいます。
- 2台構成以上の場合でも1台構成と同様にアクセスしていたサーバがダウンしたら操作した内容を保持したセッション情報は消えてしまいます。
- サーバで障害が起きてしまった場合にカートの中身が消えてしまったら「イラッ」としますよね。これはユーザの利便性を著しく下げる結果になりますのでセッションの永続化を考える必要がでてきます。
セッションの維持について
- 2台構成の場合は1台構成では考えなくて良かったことを考える必要が出てきます。それがセッションの維持です。
- サーバにアクセスするとJSESSIONIDというCookieが発行されそれに紐付く形でサーバのメモリ上にセッション情報が保持されます。ロードバランサを2台構成の手前に配置し、振分方式がラウンドロビンにしているとユーザは毎回違うサーバにアクセスすることになます。これでは前回アクセスしたときに生成したセッション情報は利用できなくなります。つまり、2台以上の構成の場合はユーザはいつも同じサーバにアクセスさせる必要がでてくるのです。これを実現するためにロードバランサにはStickyという機能があります。(ないものもあるかもしれません)
- Stickyとは、ユーザとサーバのセッションを紐付けるために使用する機能です。たとえば、同一のIPからアクセスされたら常に同じサーバにフォワードすることや、ロードバランサでCookieを発行させそのCookieに基づいてフォワード先を決めることが可能になります。
- AWSのElasticLoadBalancerもStickyをサポートしており、ユーザが発行したCookieをもとにフォワード先を決めることや、ELB自身でCookieを発行させることも可能です。
負荷の偏り
- セッションの維持設定を行った結果、複数台構成でもサービスが提供できる環境が実現できました。ただ、セッション情報は永続化していないため障害時にはショッピングカートの情報は消えてしまうことと共に別の問題がでてきます。それが負荷の偏りです。
- ユーザは身勝手です。TOPページだけみてページを閉じる短いセッションのユーザもいれば、検索してカートに保存してお気に入り登録して決済したり・・・というセッションが長いユーザがいます。
- 複数台サーバ構成でセッションが長いユーザだけある特定のサーバに偏っていたらどうなるでしょうか。(今のところ)ある特定のサーバをオンラインでスケールアップすることはできませんので運用する側としては見守るしかありません。また、実際に負荷の高いサーバに振られたユーザはレスポンスが悪く購入を諦めてしまうかもしれません。
- 性能はユーザビリティの大事なポイントです。おろそかにするとユーザはサイトを離脱して二度と使ってくれないかもしれないのです。
- 複数台のサーバで書き込むことができるリソースを用意することで特定のサーバに負荷を偏らせないように実現する方がよさそうです。
ここまででなんでセッションを永続化する必要があるのかが理解できたと思います。
セッションを保持するリソースを用意する
- 「複数台書き込むことができるリソースを用意する」とざっくり書きましたが、Javaで作ったWebアプリケーションでセッションを共有する方法は色々あります。
- 良くWebで見かけるのはtomcatのセッションレプリケーション機能です。共有のリソースを用意するのではなく、マルチキャスト通信を使って自身が持っているセッション情報を他のサーバに渡してあげる方式です(A,B,Cというサーバ各々に設定を行いAでセッション情報が書き込まれたらB,Cサーバに同期をさせる)サーバ台数分のセッション情報を各サーバに保持する必要があるためメモリを多めに積む必要があるのと内部ネットワークの負荷が気になりますね。10台APサーバがありまーすといったときを考えるとすぐ破たんしそうです。
- 共有のメモリを利用する方法もあります。WeblogicであればCoherence(有償です)というプロダクトを使って実現できますし、OSSだとRedisでクラスタを組んでTomcatからアクセスさせる方法があります。インメモリDBはとっても高速なのでFITしそうですね。
- 共有のDBにを利用する方法もよく見ます。
- RDBの場合はセッションIDをキーにセッション情報を格納するカラムを用意して書き込みます。ただ、RDBでは大量データの保持には向いていないので、セッション情報が溜まり続けるとDBへの負荷が増えていき、終いには保持しきれない。という事態になります。(これは実際のプロジェクトで見たことがあります)
- セッション情報はリレーションも必要ありませんし、大量データが保持できるNOSQLデータベースは凄くよさそうですね。HBase(NOSQL)なんていいんじゃないの?と思うのですがHBaseをクラスタ構成にすると台数がかさむのと維持管理が難しくので、ゲンナリすること間違いなしです。
セッション維持 on AWS
tomcatのセッションレプリケーション
- tomcatのセッションレプリケーション機能ですが、これはAWSでは利用できません。
- なぜならばVPCではIPマルチキャスト / ブロードキャストはサポートされないからです。これは他のクラウドでも大体同じみたいです。
- マルチキャストなどを実現するにはトンネルすればできそうですが、基本的にAWSが何かしてくれるわけではなく自身で実装が必要なので自己責任となりますのでわざわざこんなことしないですよね。
インメモリDB と KeyValueStore
- インメモリDBであればElastiCacheが思い浮かび、KeyValueStoreであればDynamoDBが思い浮かびます。
- ElasticCacheはMemcachedとRedisをサポートしています。VPC内に配置できMultiAZ構成が組めることと、キャッシュノードをどんどん追加できるというのが大きなポイント。料金はインスタンスタイプによります。
- DynamoDBはフルマネージドなサービスでVPC外のエンドポイントに存在します。フルマネージドなサービスなので自分で面倒を見る必要はありません。HTTPでAPIを叩くことでデータの保管、ロード、削除などの操作が可能です。プロビジョンドスループット、容量の上限がない。ってところもいいですね。料金はちょっとややこしくプロビジョンドスループットと容量で課金されます。
ElastiCache と DynamoDBどっち使う?
- ElastiCacheはMultiAZ間でデータ保持、DynamoDBはリージョンのすべてのAZで保持するため、データ保全という意味ではDynamoの方がやや上か。
- DynamoDBはインターネット越しの通信となることもあり、ElastiCacheの方が速そう(VPC内通信&インメモリでデータ保持)
- DynamoDBはフルマネージドなサービスでありサーバの運用が基本的に不要であり、スケール時もキャッシュノードを追加するのではなく料金プランを上げるイメージ(プロビジョンドスループットは動的に変更可能)
- DynamoDBはAWSがTomcatSessionManagerを提供している(2.0.0はtomcat8対応)
実装部分については今回利用するSpringBootであればSpringSession使えばRedisでの永続化を実現できるのであいこかなと思いました。管理の手間とスケールしやすさなどを考えるとDynamoDBかな?とうことで早速TomcatSessionManagerを使ったセッションクラスタリングを試してみました。
DynamoDB Tomcat Session Manager
jarを用意する
- SessionManagerの2.0.0版は現状ではMavenのCentralRepositoryにありませんので、jarをダウンロードするか自分でビルドする必要があります。
- [ダウンロードする場合はこちら(https://github.com/aws/aws-dynamodb-session-tomcat/releases/tag/v2.0.0)
- ビルドする場合はリポジトリをcloneして、mvn packageします。
- できたjarをクラスパスに追加します。
SpringBoot(Embedded Tomcat)に組み込む
- SpringBoot起動時にSessionManagerを設定します。解りやすいようにベタベタのコードで書いておきました。
DynamoDBSessionClusteringConfig.java
package jp.gr.java_conf.uzresk.sbex.web.config;
import org.apache.catalina.Context;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.amazonaws.services.dynamodb.sessionmanager.DynamoDBSessionManager;
import com.amazonaws.tomcatsessionmanager.amazonaws.services.s3.model.Region;
@Configuration
public class DynamoDBSessionClusteringConfig {
	@Bean
	EmbeddedServletContainerFactory configure() {
		final TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
		factory.addContextCustomizers(new TomcatContextCustomizer() {
			@Override
			public void customize(Context context) {
				context.setBackgroundProcessorDelay(1);
				context.setManager(dynamoDBSessionManager());
			}
		});
		return factory;
	}
	
	private DynamoDBSessionManager dynamoDBSessionManager() {
		 final DynamoDBSessionManager manager = new DynamoDBSessionManager();
		 manager.setAwsAccessKey("access key");
		 manager.setAwsSecretKey("secret key");
		 manager.setReadCapacityUnits(10);
		 manager.setWriteCapacityUnits(5);
		 manager.setRegionId(Region.AP_Tokyo.getFirstRegionId());
		 manager.setCreateIfNotExist(true);
		 manager.setProxyHost("proxy settings");
		 manager.setProxyPort(proxyport);
		 // session alive time(sec)
		 manager.setSessionMaxAliveTime(30);
		 // セッション永続化のタイミング
		 // maxIdleBackup + processExpiresFrequency * engine.backgroundProcessorDelay
		 manager.setMaxIdleBackup(1);
		 manager.setProcessExpiresFrequency(1);
		 // Restart時に永続化するか否か
		 manager.setSaveOnRestart(true);
		 return manager;
	}
}
各パラメータの説明
| パラメータ | 説明 | 
|---|---|
| AwsAccessKey | Awsのアクセスキー。IAMで発行します。 | 
| AwsSecretKey | AwsのSecretキー。IAMで発行します。 | 
| ReadCapacityUnits | 読み込みキャパシティユニット数を設定します。 | 
| WriteCapacityUnits | 書き込みキャパシティユニット数を設定します。 | 
| RegionId | リージョンIDです。ここでは東京リージョンを指定しています。 | 
| ProxyHost,ProxyPort | プロキシのホスト&ポート | 
| SaveOnRestart | Restart時に永続化するか否か | 
| CreateIfNotExist | DynamoDBにセッション保持するテーブルが無い場合に作るか否か | 
各パラメータの説明(補足)
- CreateIfNotExistをtrueにしておくとDynamoDBにテーブルが無い場合はTomcat_SessionStateというテーブルを自動的に作ってくれます。SpringBootの場合は起動時に@Configurationされたタイミングで作成されます。
- セッションのテーブル名も変更することが可能です。Tableパラメータを付与します。
- セッションの書き込みタイミングは「maxIdleBackup + processExpiresFrequency * engine.backgroundProcessorDelay」という計算式になります。backgroundProcessorDelayはcontext側で設定を行い、maxIdleBackupとprocessExpiresFrequencyはSessionManagerに設定を行います。毎2秒後に書き込みを開始します。session.setAttributeされたタイミングではないことに注意が必要です。
- SaveOnRestartはShutdownHookが動いたときに動きます。プロセスをブチッと切った(切れた)時には動きませんので注意が必要です。
- SaveOnRestartがちゃんと動いているか否かをSpringBootで試すのであればactuatorを使用し、application.ymlに下記を追加後、POSTで/shutdownを呼び出すことでshutdown時に永続化されることが確認できます。
endpoints:
    shutdown:
        enabled: true
SpringBootを使用しない場合
- context.xmlに同様の設定を書くことが可能です。各パラメータの設定内容は同様なので省略します。
<?xml version="1.0" encoding="UTF-8"?>
  <Context>
      <WatchedResource>WEB-INF/web.xml</WatchedResource>
      <Manager className="com.amazonaws.services.dynamodb.sessionmanager.DynamoDBSessionManager"
               createIfNotExist="true" />
  </Context>
session クラスタリングした場合の実装時の注意点
- 一番気を付けたいのはsession.getAttributeした後に値をセット後、setAttributeし忘れると値が設定されない点です。
- tomcatのデフォルトの動きではsession.getAttributeしたオブジェクトに値を設定しなおすと参照が差し替わるのでsetAttributeしなくて良いのですが永続化した場合はそのような動きになりません。(同じ参照だから大丈夫だろうというコーディングの仕方がそもそもアレですが・・・)
その他気になる点
AWSのアクセスキーの扱い
- @Valueを使って、application.ymlに外部化したとしても設定ファイルにAccessKey/SecretKeyが残るのは嫌ですね。EC2のインスタンスにロールを振っておくことでSTSが効いてくれないかなと期待してますが試してません。
終わりに
- 
DynamoDBSessionManagerの利用は非常に簡単でした。実案件前にはセッションを書き込むタイミングをどうするかについては再度検討が必要そうですね。 
- 
DynamoDBでTomcatのセッション共有をするとハマるかものエントリもあわせてどうぞこの部分をさらに詳しく解説してくれています。 
