谷本 心 in せろ部屋 このページをアンテナに追加 RSSフィード

2016-01-06

[]AWS Lambda + Javaは、なぜ1回目と3回目の処理が重いのか?

以前のエントリーで、AWS LambdaでJavaを使ってDynamoDBを呼び出した際に、初回起動にとても時間が掛かったという話を書きました。

http://d.hatena.ne.jp/cero-t/20160101/1451665326


今回は、この辺りの原因をもう少し追求してみます。


なぜ1回目と3回目のアクセスが遅いのか?

AWS Lambdaの中身はよく知りませんが、おそらく、アップロードしたモジュールTomcatみたいなコンテナとして起動させて、外部からコールしているんだろうと予想しました。それであれば、2回目以降のアクセスが早くなることは理解ができます。

ただ、1回目と3回目だけが極端に遅くて、2回目、4回目以降は早くなるというところは腑に落ちません。


その辺りを調べるべく、staticなカウンタを使って、値がどんな風に変化するかを調べてみました。

こんなソースコードです。

public class Pid {
    static AtomicLong counter = new AtomicLong();

    public String myHandler() {
        long count = counter.incrementAndGet();
        String name = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println("Name: " + name);
        System.out.println("Count: " + count);
        return "SUCCESS";
    }
}

出力された結果は、次のようになりました。

回数NameCount
1回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal1
2回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal2
3回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal1
4回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal2
5回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal3
6回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal4
7回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal5
8回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal3
9回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal4
10回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal5
11回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal6
12回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal7

サーバIPアドレスが2種類あり、それぞれのサーバで、1から順番にカウントアップしていることが分かります。

なるほど、2台のサーバでロードバランシングしているのだと。そのため、それぞれのサーバの初回起動である、1回目と3回目の処理に時間が掛かるのですね。なかなか納得いく結果でした。

ちなみにロードバランシングは毎回このような結果になるわけではなく、1回目と2回目がそれぞれ別のサーバに行く(=処理に時間が掛かる)こともあります。


どんなコンテナを使っているのか?

先ほど「Tomcatみたいなコンテナ」を使っているんじゃないかと推測しましたが、実際、どんなコンテナを使っているのでしょうか。スレッドダンプを取って、確かめてみました。


こんなコードです。

public class StackTrace {
    public String myHandler() {
        new Exception("For stack trace").printStackTrace();
        Arrays.stream(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true))
                .forEach(System.out::println);
        return "SUCCESS";
    }
}

結果、こうなりました。

java.lang.Exception: For stack trace
	at cero.ninja.aws.analyze.StackTrace.myHandler(StackTrace.java:8)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at lambdainternal.EventHandlerLoader$PojoMethodRequestHandler.handleRequest(EventHandlerLoader.java:434)
	at lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:365)
	at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:967)
	at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:231)
	at lambdainternal.AWSLambda.<clinit>(AWSLambda.java:59)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:93)

"Signal Dispatcher" Id=4 RUNNABLE

"Finalizer" Id=3 WAITING on java.lang.ref.ReferenceQueue$Lock@19bb089b
	at java.lang.Object.wait(Native Method)
(略)

"Reference Handler" Id=2 WAITING on java.lang.ref.Reference$Lock@4563e9ab
	at java.lang.Object.wait(Native Method)
(略)

"main" Id=1 RUNNABLE
	at sun.management.ThreadImpl.dumpThreads0(Native Method)
	at sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:446)
	at cero.ninja.aws.analyze.StackTrace.myHandler(StackTrace.java:9)
(略)

何やらシンプルな独自コンテナを使っているみたいです。何度か実行してみても結果は同じでした。

ソースコードがないので推測になりますが、アップロードされたzipをロードして起動する独自コンテナがあり、外部からAPIコールされた際にAWSLambdaクラスあたりが処理を受け取って、zip内のハンドラを呼び出しているのでしょう。


なぜコンストラクタで処理すると早いの?

そういえば、もう一つ、謎な挙動がありました。

それは、Credentialsを取るという重めの処理をコンストラクタで実行すれば、処理時間がかなり短くなるというものです。


ハンドラの中でCredentialsを取ると、実測値で24秒ぐらい、課金対象値で22秒ぐらいでした。

コンストラクタでCredentialsを取っておくと、実測値で8秒ぐらい、課金対象値で6秒ぐらいでした。

ここでいう実測値とは、手元のストップウォッチを使って計測したという意味です。


ここから推測できることは、コンストラクタは事前に処理されていて、そこは課金対象外になるのかも知れません。


・・・あれ、それなら、コンストラクタで重い処理をがっつり走らせて、ハンドラでその結果を取り出せば、課金額を抑えられるじゃないですか?

ということで、ハンドラの中で10秒スリープする場合と、コンストラクタスリープした場合の比較をしてみました。


こんな2つのクラスで試してみます。

public class Wait1 {
    static long origin = System.currentTimeMillis();

    public String myHandler() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(10000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "SUCCESS";
    }
}
public class Wait2 {
    static long origin = System.currentTimeMillis();

    public Wait2() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(10000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String myHandler() {
        System.out.println("Called: " + (System.currentTimeMillis() - origin));
        return "SUCCESS";
    }
}

結果は、、、


Wait1(ハンドラの中でsleep)

回数Before waitAftre wait課金対象値実測値
1回目3901039010100ms10秒
2回目3871038710100ms10秒
3回目209173091710100ms10秒
4回目688527885210100ms10秒

きっちり10秒sleepして、課金対象値もそのオーバーヘッド分ぐらい。ストップウォッチで計測した値も同じく10秒ぐらいになりました。


Wait2(コンストラクタでsleep)

回数Before waitAftre wait課金対象値実測値
1回目3381033814800ms25秒
2回目--100ms1秒以下
3回目2611035814800ms25秒
4回目--100ms1秒以下

えーっ、sleepは10秒だったのに、なぜか15秒分ぐらい課金されてしまい、ストップウォッチで計測すると25秒と、えらく時間が掛かりました。これは謎な挙動です。

2回目や4回目ではインスタンス生成が終わっているので、Before waitやAfter waitが出力されず、処理がすぐに終わるというのは納得ですが。


どうして、こんなことが起きるんでしょうか。

不思議に思って、CloudWatch Logsのログを確認してみると・・・

Before wait: 17 
START RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f Version: $LATEST 
Before wait: 249 
After wait: 10250 
Called: 14408
END RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f 
REPORT RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f	Duration: 14531.57 ms	Billed Duration: 14600 ms Memory Size: 128 MB Max Memory Used: 27 MB	

最初のBefore waitの後にAfter waitがなく、Lambdaの処理がSTARTした後に再度Before waitが呼ばれ、After waitした後に、4秒ほど待ってから、ハンドラ処理が実行されてCalledが呼ばれていました。

なるほど、つまりこういうことでしょうか。


1. コンストラクタが実行され、10秒sleepの途中でタイムアウトして強制的に処理が止められ、インスタンス生成を諦めた(プロセスごと破棄された?)

2. 改めてコンストラクタが実行され、10秒sleepした。

3. AWS Lambda内の処理か何かで4秒ぐらい処理が掛かった。

4. ハンドラが実行された。

5. 2〜4の間が課金対象となり、15秒弱となった。

6. ストップウォッチで計測した時間は1から5までの間なので、25秒弱となった。


要するに、コンストラクタで重い処理を行うような悪いことを考える人への対策として、コンストラクタは一定時間で(おそらく10秒きっかりで)タイムアウトして、いったんプロセスは破棄される。

その後、改めてコンストラクタの処理がタイムアウト関係なく実行されたうえで、AWS Lambdaの内部処理と、ハンドラ処理が行われ、すべての処理が課金対象となる、ということころでしょうか。


コンストラクタの処理が短い場合は、どうなるの?

ということで、sleepの時間を短くして、再挑戦してみます。

public class Wait3 {
    static long origin = System.currentTimeMillis();

    public Wait3() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(2000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String myHandler() {
        System.out.println("Called: " + (System.currentTimeMillis() - origin));
        return "SUCCESS";
    }
}

結果。

回数Before WaitAfter wait課金対象値実測値
1回目--100ms3秒
2回目--100ms1秒以内
3回目--100ms1秒以内

CloudWatch Logsでの出力

Before wait: 25 
After wait: 2026 
START RequestId: xxx Version: $LATEST 
Called: 2389 
END RequestId: xxx 
REPORT RequestId: xxx	Duration: 97.52 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 27 MB	 
START RequestId: yyy Version: $LATEST 
Called: 16385 
END RequestId: yyy 
REPORT RequestId: yyy	Duration: 6.04 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 27 MB	

今度はSTARTの前に、きちんとBefore waitもAfter waitも出力され、ハンドラ処理のみが課金対象となっていました。


まとめ

ここまでの話をまとめると、AWS Lambdaは・・・


1. 複数台のサーバで処理されるため、それぞれのサーバでの初回起動時には処理時間が掛かる。

2. 独自のコンテナを利用して、モジュールデプロイしている。

3. コンストラクタの処理が軽い場合は、ハンドラ内の処理だけが課金対象となる。

4. コンストラクタの処理が重い場合は、コンストラクタの処理 + 5秒弱 + ハンドラ内の処理が課金対象となる。


ということですね。


・・・とは言え、Credentialsの処理をコンストラクタで行った場合に、実測値まで早くなる辺りは、少しだけ不可解です。というか、Credentialsの取得処理が重いこと自体が不可解なのですが。

この辺りはもう少し追試験をしてみれば解析できそうですが、長くなるので、今回はこの辺りまでにしたいと思います。


いやー、Lambdaさん、なかなかよく考えられてますね!

2016-01-01

[]AWS LambdaでJavaNode.jsとGoの簡易ベンチマークをしてみた。

あけましておめでとうございます!

現場でいつも締め切りに追われデスマーチ化している皆様方におかれましては、年賀状デスマーチ化していたのではないかと憂慮しておりますが、いかがお過ごしですか。

エンジニアの鑑みたいな僕としましては、年始の挨拶はSNSでサクッと済ませ、年末年始は紅白など見ながらAWS Lambdaのソースコードを書いていました。


ということで、Lambda。

Lambdaを書く時に最初に悩むのは、どの言語を選択するか、なのです。


まず手を出すのは、サクッと書けるNode.js

ただNode.jsの非同期なプログラミングになかなか馴染めず、わからん殺しをされてうんざりしていました。みんなどうしているのよとtwitterで問いかけたところ @ からPromiseを使うべしと教わり、なるほど確かにこれは便利ですナと思いつつも、これが標準で使えないぐらいLambdaのNode.jsが古いところにまたうんざりしました。


では手に馴染んでるJavaを使ってみたらどうかと思ったら、メモリイーターだし、なんとなくパフォーマンスが悪い感じでした。詳しいことは後ほどベンチマーク結果で明らかになるわけですが。


それなら消去法で残ったPythonなのですが、僕マジPython触ったことないレベルであり、これは若者たちに任せることにしているので、Pythonも選択肢から消えて。


本来、Lambdaみたいな用途にはGoが向いているはずなのだけど、いつ使えるようになるんだろうなぁなどと思って調べてみたら、Node.jsからGoを呼び出すというテクを見つけて、こりゃいいやとなりました。

http://blog.0x82.com/2014/11/24/aws-lambda-functions-in-go/


ただGoを呼ぶにしてもNode.jsを経由するためのオーバーヘッドはあるわけで、じゃぁ実際にどれぐらいパフォーマンス影響があるのか、調べてみることにしました。


処理の中身

簡単に試したいだけだったので、数行で済む程度のごく簡単な処理を書いてベンチマークすることにしました。


1. 引数で渡されたJSONパースして、中身を表示する

2. DynamoDBから1件のデータを取得する


当初は1だけだったのですが、あまり性能に差が出なかったので2を追加した感じです。そんな経緯のベンチマークなので、実装も雑ですが、とりあえずソースをGitHubに置いておきました。

https://github.com/cero-t/lambda-benchmark


実行結果
回数Java(1)Java(2)Node.jsGo
1回目218006700900600
2回目13007300800500
3回目19000500200500
4回目10001300200400
5回目500200400500
6回目400100200500
7回目400300400500

時間はいずれもミリ秒のBilled duration


Java(1) : メモリ192MB。処理中にAmazonDynamoDBClientを初期化

Java(2) : メモリ192MB。処理前に(コンストラクタで)AmazonDynamoDBClientを初期化

Node.js : メモリ128MB

Go : メモリ128MB


メモリの実使用量は

Java : 68MB

Node.js : 29MB

Go : 15MB

でした。


考察など

正味の話、Lambdaで行っている処理などほとんどないので、性能的には大差ないかと思っていたのですが、思ったより特性が出ました。これは処理時間というよりは、関連ライブラリのロード時間な気がしますね。


Javaは最初の数回が20秒、実装を改善しても7秒とかなり遅かったのですが、ウォーミングアップが終わった後は200msぐらいまで早くなりました。呼ばれる頻度が高い処理であれば良いのでしょうけど、たまに呼ばれるような処理では、この特性はネックになる気がします。


ちなみにJavaだけは192MBで計測しましたが、最初にメモリ128MBで試したところ、30秒ぐらいで処理が強制的に中断されたり、60秒でタイムアウトするなど、計測になりませんでした。こういう所を見ても、Javaを使う時にはメモリを多め(CPU性能はメモリと比例して向上)にしなくてはいけない感じでした。


Node.jsも少しはウォーミングアップで性能が変わりますが、最初から性能は良い感じです。


Goを使った場合は性能が安定しており、ウォーミングアップしても性能が変わりません。メモリ使用量が少ないのも良いですね。Node.jsから呼び出す際のオーバーヘッドがあるせいか、性能的にはウォーミングアップ後のJavaNode.jsに一歩劣る感じでした。


なお、Pythonの実装をしてくれる人がいらっしゃれば、プルリクしていただければ、データを追加したいと思います。


結論

繰り返しになりますが、雑なベンチマークなので特性の一部しか掴んでないと思います。

そもそもJavaだけメモリが1.5倍になっている(=CPU性能も1.5倍になっている)ので公平ではないですし。


ただ「たまに行う処理」を「少ないリソース」で行うという観点では、Goで実装するのが良さそうです。GoはそのうちLambdaでも正式サポートされそうですしね。

きちんとしたベンチマークは、また別の機会にじっくり行ってみたいと思います。


ということで、Goを使う大義名分ができたベンチマークでした!

See you!

2015-10-19

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

AWSを仕事で使い始めて1年半、

ようやく頭がクラウド脳に切り替わってきた @ です。

好きな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管理しようぜ。


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