谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

DynamoDBでTomcatのセッション共有をするとハマるかも

AWSを仕事で使い始めて1年半、
ようやく頭がクラウド脳に切り替わってきた [twitter:@cero_t] です。
好きなAWSサービスはKinesisです。まだ使ってませんけどね!


さて、今日のテーマは「AWSTomcatのセッション共有」です。
EC2上で動くTomcatのセッションオブジェクトを、DynamoDBを使って共有するというものです。


話題としてはそれなりに枯れていると思うのですが、
実案件で使おうと思ったら問題が出そうになって困ってる、という話です。

発生する問題は?

どういう問題が起きるか、先に書いておきます。


発生する問題は、
複数のTomcatをELBで分散させている時に、
スケールインやスケールアウトが短時間に連続して発生すると、
セッションが巻き戻る(先祖返りする)可能性がある、というものです。


セッションが消えるならまだしも、
先祖返りするというのは、実案件において許されない感じです。


あっ、
そもそも「セッションなんか使うから問題が起きるんだ」というツッコミはナシでお願いします。
それは分かったうえで、やむを得ずセッションを使うならどうしようか、という検討なのです。

Tomcat + DynamoDBの組み合わせ方

Tomcatのセッション共有になぜDynamoDBを使うのか、どういう設定をするのか、
というのは、この辺りのエントリーで学びました。


Amazon DynamoDBによるTomcatセッション永続化とフェイルオーバー - Developers.IO
Tomcat 7.x時代の記事ですが、考え方や注意点がとても分かりやすく紹介されています。


AWSでセッションをクラスタリングする方法について考えてみた結果、DynamoDBがよさそうなので試してみた。 - Qiita
Tomcat8 / Spring Boot / DynamoDBの連携がかなり詳しく紹介されています。


これらのサイトでも紹介されている内容を踏まえると、
以下のような流れになりそうです。

1. ELBの設定でスティッキーにして、同一セッションIDは同じTomcatに振り分ける
2. TomcatからDynamoDBに非同期で書き込む。遅延時間は調整可能(最低1秒?)
3. Tomcatがセッションを持っていない場合に限り、DynamoDBを参照する。
 (スケールイン / スケールアウトなどが起きた時に、セッションを引き継ぐことができる)


この流れは、いわゆる「リードスルー方式」と「ライトビハインド方式」を組み合わせたものだと言えます。
一見、この流れで問題がなさそうなのですが、
よくよく考えるとセッションの巻き戻しが起きることが分かりました。

どういう時に問題が起きる?

問題の再現状況は以下の通りです。

  • ELB
  • Tomcat 2台(仮にTomcat1、Tomcat2と呼ぶ)
  • DynamoDB

この構成で「フェイルオーバー → 復帰 → フェイルオーバー」を、
セッションタイムアウトよりも短い時間内で繰り返すと、問題が発生します。


時系列で順を追って説明しますね。

(1) Tomcat1 + DynamoDBにセッション保持(Tomcat1 → DynamoDB)

ユーザがアクセスした際に、ELBによってTomcat1に振り分けられたとします。
以後、このユーザは必ずTomcat1に振り分けられるため
ユーザがセッションに書き込んだ内容は、Tomcat1からDynamoDBに永続化され
Tomcat1のメモリとDynamoDBの両方で保持されることになります。

(2) フェイルオーバー(DynamoDB → Tomcat2)

ここでELBからTomcat1への振り分けを遮断すると、
ユーザのアクセスは、ELBによってTomcat2に振り分けられます。
ここでセッション情報はDynamoDBからTomcat2にロードされるため
これまで蓄積してきたセッション情報が消失することも、巻き戻ることもありません。

(3) Tomcat2 + DynamoDBにセッション保持(Tomcat2 → DynamoDB)

ユーザのアクセスはTomcat2に振り分けられていますので
ユーザがセッションに書き込んだ内容は、
Tomcat2のメモリとDynamoDBの両方で保持されます。

(4) Tomcat1の復帰(DynamoDB → Tomcat1)

次に、ELBからTomcat1への振り分けを再開すると
ユーザのアクセスは、Tomcat1に振り分けられます。
(Tomcat2に固定されず、Tomcat1に戻るんですよね)


Tomcat1を一度再起動するなどして、メモリにあったセッション情報を空にしておけば
最新のセッション情報がDynamoDBからTomcat1にロードされるため
やはりセッションの巻き戻しはありません。

(5) Tomcat1 + DynamoDBにセッション保持(Tomcat1 → DynamoDB)

この状況でユーザがセッションに書き込んだ内容は、
Tomcat1のメモリとDynamoDBの両方で保持されます。
ここまでは問題ありません。

(6) 改めてフェイルオーバー(Tomcat2のみ)

ここで再度ELBからTomcat1への振り分けを遮断すると、
ユーザのアクセスは、ELBによってTomcat2に振り分けられます。


この時、Tomcat2のメモリ内には (3) の時に書き込んだセッション情報が存在するため
わざわざDynamoDBを読みに行かず、Tomcat2が保持しているセッション情報を利用します。
そのため (5) で更新した内容から (3) の内容まで、巻き戻しが発生してしまいます。


つまり、0〜1秒ぐらいのタイミング問題(避けられない事故)ならまだしも、
数分ぐらいのオペレーションでも、セッションの巻き戻りが起きることになります。

じゃぁどうするの?

ここまで見てきた通り、書き込みが非同期である以上、
巻き戻りの問題が発生することは避けられません。


もちろん運用上、このような操作(短時間でのフェイルオーバー)を行なわないようにするのは一つの解ですが、
たとえば「リリース失敗時の切り戻し」なんてことを考えると、発生する可能性はゼロではありません。
そのため「買い物カゴ」のような、巻き戻りが業務に影響してしまうものは、この方式では扱えません。


では、設定を修正して何とかできないか考えてみます。
そもそも「非同期書き込み」と言えば、リードスルー / ライトビハインド方式以外にも
 1. ライトスルー方式(DynamoDBの更新時に、全Tomcatも同時に更新する)
 2. Tomcatのメモリを一切使わない(セッションの読み書き時には必ずDynamoDBを利用する)
の2つが考えられます。


ただ、TomcatのPersistenceManagerを利用する限りは、
どう設定しても1にも2にもならないことが分かりました。


そもそもPersistenceManagerはセッション共有の仕組みではなく、
JavaVMのヒープを過剰に占有しないために永続化するものです。
それをセッション共有のために代用しているだけであり、
本気でセッション共有を考えられたものではありませんでした。


・・・という事で、今回は、セッションを利用することを諦めました (^^;
最初に「セッション使うなっていうツッコミはナシ」とか言っておきながら、すみません (^^;;


まず「買い物カゴ」のような、決して巻き戻ってはいけない重要な情報は
セッションで扱うことを諦め、直接DBに永続化することにしました。


一方で「ログイン情報」や「行動をトレースするための情報」など、
最悪、多少巻き戻っても業務に影響しないような情報のみ、セッションに残すようにしました。


ちゃんちゃん♪

他の選択肢 - Spring Session

そんなわけで、DynamoDB、というか、
PersistenceManagerを利用したセッション共有は、お手軽にできるものの、
問題がありそうだという結論に至りました。


それでもセッション共有は諦めきれず、
他の選択肢として Spring Session を確認してみました。
Spring Sessionは、Redisなどをバックエンドとして利用できるセッション共有の仕組みです。


軽くソースを読んでみたところ
 1. session#setAttributeすると「差分データ」として内部で保持される
 2. ServletFilterで、処理の終了後に「差分データ」をまとめてRedisに反映させる
 3. session#getAttributeしたオブジェクトに対して操作しても、差分は反映されないので注意
ということが分かりました。
この仕組みなら、フェイルオーバー時の挙動もあまり問題にならなさそうです。


ただ、2の通り、セッションには即時反映せず、リクエスト終了時に反映するため、
たとえば同一セッションで複数リクエストを処理する際には、
(たとえば同一セッションから同時にアクセスカウンターをインクリメントしようとしても)
上手くいかないことがありそうです。


即時反映をサポートをして欲しいというチケットが挙がっているので、
将来的には即時反映がサポートされるかも知れません。
https://github.com/spring-projects/spring-session/issues/250


また3の制約により、既存のアプリケーションにSpring Sessionを適用する場合には
多少ソースを改修しなければいけないこともあるでしょう。


そんな理由で、今回は採用を見送りました。
今後の案件でSessionを使わざるを得ない場合に、改めて検証してみたいと思います。

まとめ

1. TomcatのPersistenceManager + DynamoDBなどによるセッション共有は、
 書き込み遅延や、フェイルオーバー時の巻き戻りを受け入れざるを得ない。


2. Spring Session + Redisによるセッション共有は、
 同一セッションでの複数リクエストへの考慮をすること、
 セッション更新時に、きちんとsession#setAttributeすることをルール化すれば、
 それなりにきちんと使えそう。


3. そもそも、絶対に巻き戻っちゃいけないトランザクショナルなデータは
 セッションなんてあいまいなものに持たせず、RDB管理しようぜ。


まぁ結局、セッション使うなという所に戻ってくるのは、何ともですね。