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

2017-12-07

[][]Goで格闘ゲームマクロを実装してみた

このところ、夜な夜な格ゲーの動画を撮ってはYoutubeにアップしては、SRK(shoryuken.com)に取り上げられて承認欲求を満たしている @ です。SRKは世界最大の格ゲーコミュニティだよ!

http://shoryuken.com/?s=Shin+Tanimoto


このエントリーは Go2 Advent Calendar 2017 の7日目です。

https://qiita.com/advent-calendar/2017/go2


はじめに

まえがき

学生時代、僕は格闘ゲームの連続技(コンボ)の研究を嗜んでおりまして、学校に行くぐらいならゲーセンを行く、試験勉強するぐらいならコンボの研究をする、卒論を書くぐらいならコンボビデオを撮るという、クズみたいな学生生活を送っておりました。当時、いまは亡き「ゲーメスト」というアーケードゲーム雑誌で体験ライターとして編集部に泊まり込んで記事を書くような体験もしました。

その頃よくプレイしていたストリートファイター系のゲームが、なんと18年の歳月を経て新作を出すというのです。これは万難を排してやらねばならぬと相成ったのですが、当時20歳前後だった僕も先日ついに40歳を迎えたわけで、さすがにもうあの頃のようには手が動きません。

しかし、いまの僕には、あの頃の自分にはなかったプログラミング能力があります。人間が手作業で行っていることを、機械に行わせるのが仕事です。そう、格闘ゲームも機械にやらせてしまえば良いじゃないか、というのが今回のテーマになります。


※なお、ゲームのネット対戦やオンラインランキングでマクロを使うのはマナー違反です。決してやらないでください。


目的

プレステ3プレステ4Xboxなどで使えるマクロを作ります。実装にはGoを使いました。


用意するもの

ゲーム機とPCをつなぐ、何かしらのデバイスが必要になります。今回はConsole Tuner社のTitan Oneという機器を利用します。

https://www.consoletuner.com/products/titan-one/

f:id:cero-t:20171207075818j:image:w360


これはUSBゲームコントローラーを様々なゲーム機で使えるようにするための変換器です。しかしただの変換機能だけでなく、PCと繋ぐことでマクロ機能やプログラミングを組んだりできるほか、API自体もDLLとして公開されているので、自作アプリからも利用することができます。今回の用途にはベストです。


Titan OneとDLLを使って、次のような流れで操作を流し込みます。

ゲーム機 <-(USB)- Titan One <-(USB)- Windows(DLL <- 自作アプリケーション)

なんとなくイメージが湧くでしょうか?


なぜアプリを自作するのか?

先ほど「マクロ機能やプログラミング環境が用意されている」と書いた通り、用意された環境を利用すれば、別にアプリを自作するまでもなくマクロを実行することができます。ただ、付属のプログラミング環境ではミリ秒単位の制御しかできず、格闘ゲームの60fps(framer per second)の制御をするには不便でした。格闘ゲーマーは物事をフレーム単位で考えていて、ミリ秒単位で考えるわけではないのです。

なので、フレーム単位で扱いやすくするための、簡単なアプリを自作することにしました。


なぜGoなのか?

今回のアプリは、配布が容易であることと、Mac/Windowsのいずれでも開発できるという点でGoを選びました。

僕はJavaエンジニアなので、最初はJavaでJNAを使って実装しました。Javaでも問題なく動くものができたのですが、ただちょっと配布するのが面倒だなと思い、かと言ってC#にするとMacでの開発環境が微妙なので、Goを選ぶに至りました。もちろんそんな消去法的な選択だけでなく、ちょっとGoを勉強してみたかったという背景もあります。


DLL呼び出しの実装

それではGoで実装していきます。僕はGo初心者で体系的に学んだこともないので、ここがおかしいよとか、ここもっとこうした方が良いよというのがあれば、教えてもらえると凄く嬉しいです。


DLLの読み込み

まずTitan Oneが提供しているDLL(gcdapi.dll)を読み込む部分を作ります。

goでDLLを読み込むにはsyscall.LoadDLLを使います。

dll, _err := syscall.LoadDLL("gcdapi.dll")
if _err != nil {
	log.Fatal("Error loading gcdapi.dll", _err)
}

これでDLLの読み込みができます。


ここで「Failed to load gcdapi.dll: %1 is not a valid Win32 application.」というエラーに悩まされました。

原因は「gcdapi.dllが32bit」だったことです。64bit環境でコンパイル/実行しようとするとエラーになるので、32bit環境向けにコンパイルする必要があります。

set GOARCH=386

これでOK。

ここで1時間ほどハマっていました。


DLL内の関数呼び出し

DLLにある関数を呼び出すには、読み込んだdllのFindProc関数を使います。

gcdapi_Load, _err := dll.FindProc("gcdapi_Load")
if _err != nil {
	log.Fatal("cannot find gcdapi_Load", _err)
}

result, _, _err := gcdapi_Load.Call()
if result == 0 {
	log.Fatal("gcdapi cannot be loaded")
}

これでgcdapi_Loadという関数を実行できます。

DLL内にある他の関数も、同じ要領で先に読み込んでおきます。


また、関数引数を渡す必要がある場合は、uintptr型のポインタとして渡します。

次の例は、someFunction関数にarg1という値を渡す例です。

result, _, _ := someFunction.Call(uintptr(arg1))

戻り値はresultに入ります。


配列や構造体を引数に渡す

引数配列や構造体を渡す場合は、先に参照をポインタに変換してから、さらにuintptrに変換します。

正直、参照がどうなってるのかもはや僕には理解できないのですが、こうすれば動くということだけ確認しました。

func write(inputs *[36]int8) bool {
	result, _, _ := procWrite.Call(uintptr(unsafe.Pointer(inputs)))
	// 略
	return true
}

ちなみにここで配列ではなくスライスを渡していたために上手く動かず、2時間ほどハマりました。初学者はとにかくハマって時間をロストしますね。


また構造体を引数にする場合は、DLL側に用意された構造体と同じ名前、同じ型の構造体をGo側に用意します。

次の例ではGCAPI_REPORTという構造体を引数に渡しています。

type GCAPI_REPORT struct {
	console       uint8
	controller    uint8
	led           [4]uint8
	rumble        [2]uint8
	battery_level uint8
	input         [30]GCAPI_INPUT
}

type GCAPI_INPUT struct {
	value      int8
	prev_value int8
	press_tv   uint32
}

func read() bool {
	var report GCAPI_REPORT
	result, _, _ := procRead.Call(uintptr(unsafe.Pointer(&report)))
	log.Println(report)
	// 略
	retun true
}

構造体は恐ろしいぐらいにハマらずに値が入りました。簡単です。最高です。


MacでもWindowsでも実行したい

ちなみにここまで使ってきたsyscall.LoadDLLや、その戻り値であるsyscall.DLL、syscall.ProcなどはWindows環境でしか利用できず、Mac環境でコンパイルしようとするとエラーになってしまいます。

せめてMacではDLL呼び出し部分だけモックにして、アプリ全体としては動くようにしておきたいものです。そういうのもきちんと用意されていました。


同じパッケージ内に、同名の関数を定義した別ソースファイル(モック用関数群)を作り、そのファイルの先頭にこんなコメントを入れます。

// +build !windows

これでWindowsの環境以外でビルドした場合には、モック用関数群のファイルが利用されるようになります。

・・・ちょっと何を言ってるのか分からない感じなので、実物を見て頂いたほうが早いと思います。


Windows用のソースコードがこれ。

https://github.com/cero-t/cero-macro.t1/blob/master/gcapi/gcapi_dll_windows.go


Windows以外用のソースコードはこれです。

https://github.com/cero-t/cero-macro.t1/blob/master/gcapi/gcapi_dll_other.go


いいですね、簡単です。


次に入力側を作る

DLLの呼び出しが上手く行ったので、次は呼び出し側の設計/実装です。


懐かしのコマンド

20世紀に格闘ゲームをやっていた人にとって、「236p」という文字列波動拳コマンドにしか見えません。これはテンキー配列の「下 右下 右」に相当するもので、最後のpがパンチです。当時のパソコン通信インターネットで頻繁に用いられた記法です。同様に、昇龍拳は「623p」、竜巻旋風脚は「214k」となります。今回はこの表記をします。

また、パンチの小中大はそれぞれl, m, hで表現し、たとえば小パンチは「lp」、中キックは「mk」、しゃがみ大キックは「2,hk」とします。これは海外の格闘ゲーマーが用いている表記方法です。


この表記法を使い、たとえば次のようなテキストを受け取ることを想定します。

2,mk 8
3 1
6,lp 1

これはレバー下と中キックを8フレーム、右下に1フレーム、右と小パンチを1フレーム、というコマンドです。中足波動拳ですね。

このようなコマンドを受け取って、上で説明した関数の呼び出しを行うようにします。


入力コマンドの変換

格闘ゲームの入力は精度が命ですから、Go側の処理に時間が掛かってコマンドが不安定になってしまっては意味がありません。そのため、上に書いたテキストをあらかじめパースして配列などに変換しておき、その後にまとめてDLL経由でコマンドを流し込む処理を行うようにします。変換とDLL呼び出しを逐次行っていると、想定しない処理遅延が発生する可能性があるためです。


テキストをパースした後、次のような構造体に変換します。

type State struct {
	buttons [36]int8
	frames  uint16
}

buttonsが押すべきボタンの一覧です。配列のそれぞれがボタンやレバーに対応しており、押さない時は0、押した時は100という値が入ります。


変換は地味なコードなので割愛します。ここでやってます。

https://github.com/cero-t/cero-macro.t1/blob/master/processor/converter.go


正確な60fpsのコマンド実行

続いて、コマンド実行の処理です。State構造体の配列をきっちりと指定したフレームで呼び出す必要があります。

ちなみに格闘ゲームではよく「1/60秒」という表現が使われて、「光速は遅すぎる」など話題になることがありますが、少なくとも手元環境のプレステ3で確認したところ、1フレームは「1/60秒」ではなく「1/59.94秒」でした。


そのあたりも踏まえて、次のようなコードを書きました。ボタンを押した後に、そのフレーム数分(たとえば5フレームなら、約 5 * 16.6ms = 83ms)だけ待つというコードです。

ただしこの処理自体の実行時間も考慮する必要があるため、現在時刻など利用して、フレームレートを保つようにしています。

// フレームレート (frame per 1000 seconds)
const frameRate uint64 = 59940

func Process(states *[]State) {
	var totalFrames uint64
	start := time.Now().UnixNano()

	for _, state := range *states {
		// ボタンを押す
		gcapi.Push(&state.buttons)

		// フレーム数分だけsleepする
		totalFrames += uint64(state.frames)
		sleepTime := start + int64(totalFrames*1000*1000*1000*1000/frameRate) - time.Now().UnixNano()
		time.Sleep(time.Duration(sleepTime))
	}
}

正確に59.94fpsを保つためにわざわざアプリを自作したので、この処理が一番のキモと言えますね。


この関数を含む処理全体はここにあります。

https://github.com/cero-t/cero-macro.t1/blob/master/processor/processor.go


UIはどうする?

最後にUIです。

GoのUIをどうすべきかは議論があるようですが、「クロスプラットフォームで使えて、皆が慣れ親しんでいるUI」と言えば、コマンドラインCUI)とHTMLの2つでしょう。


まずはCUI

テキストファイルを読み込んでその中に書かれた操作コマンドを実行するCUIを作ります。

こんな感じで、実行時の引数にファイル名を指定します。

$ go run macro.go combo.txt

これでcombo.txtに書かれた内容が、ゲーム機に送られるのです。


このmacro.txtという渋い名前のファイルの中身は、こんな感じです。

9 34
hp 20
2,mk 8
6 1
2 1
3,hp 1

ジャンプ大パンチ 中足 昇龍拳、という感じです。


続いてGUI

もう一つのUIが、HTTPサーバとして起動し、画面で入力されたコマンドを実行するGUIです。実行時の引数にファイル名を指定しなければHTTPサーバとして立ち上がります。

$ go run macro.go
Server started at [::]:8080

それでlocalhostの(あるいは別のPCからIPアドレスを指定して)8080ポートにアクセスすれば、こんな画面が出てきます。

f:id:cero-t:20171207075959p:image

テキストボックスにコマンドを入力して「run」を押せば実行できる形です。

マクロで動かす以外にも簡単な操作をリモートでできるよう、他のボタンも置いておきました。「LP」ボタンを押すと、「lp 2」というマクロ(小パンチを2フレーム押す)が送られます。


なおHTMLファイルの使い方ですが、Goでリソースバンドルするのがちょっと面倒だったので、ソースコード内にヒアドキュメントで書いてしまいました。

func formHTML() string {
	return `
<!DOCTYPE html>
<html>
<head>
(略)
</body>
</html>
`
}

なおViewフレームワークには超軽量で有名な「Vanilla JS」を用いているため、この1ファイルだけでViewのレンダリングサーバとの通信処理なども実装できています。

http://vanilla-js.com/

はい。


HTTPクライアントサーバ関連のソース全体はこちらです。

https://github.com/cero-t/cero-macro.t1/blob/master/http/form-html.go

https://github.com/cero-t/cero-macro.t1/blob/master/http/http.go


実際、試してみたところ・・・!!

これで画面からDLLまですべて繋がったので、いざ実機で挑戦!


おぉ、動く動く! キャラが動くよ!


・・・あれ、コンボが途切れるぞ?

なんか、コンボ不安定だぞ?


そうなんです、どうやらTitan Oneは処理遅延が10ms程度あり、その遅延も安定しないため、格闘ゲームの1フレームを安定させて動かすのが難しいようなのです。

ここまで書いてきてこのオチはなかなか酷いのですが、別にこれで終わりというわけではありません。

より遅延が少ないデバイスが出てきて、それをAPI経由で叩けるようになれば、また再挑戦したいと思います。


あれ、この手元にあるデバイスは、何だろう??

f:id:cero-t:20171207080038j:image:w360


来年には、API経由で叩けるようになるのかなぁ。

2015-12-24

[][]JMXで情報を取得する時のベンチマーク

JMHを使って、JMX経由でMBeanの情報を取る際のパフォーマンスを測定してみた。

ベンチマークソースコードはこちら。

https://github.com/cero-t/Benchmarks/blob/master/src/main/java/ninja/cero/benchmark/JmxBenchmark.java


ベンチマーク環境はMacBook Pro Late 2013 (Core i5 2.4GHz) で、

他のアプリなども立ち上げっぱなしの環境なのでノイズは多めでだけど、

傾向を見たいだけなのであまり気にせず。


VirtualMachine.attachのパフォーマンス

VMオンデマンドアタッチする際のパフォーマンス。


No1 : VirtualMachine.attacheしてからシステムプロパティを取ってdetachする

No2 : キャッシュしていたVirtualMachineを使ってシステムプロパティを取得する

@Benchmark
public void no1_vmAttach() throws Exception {
    VirtualMachine vm = VirtualMachine.attach(PID);
    vm.getSystemProperties();
    vm.detach();
}

@Benchmark
public void no2_vmCachedGetProperties() throws Exception {
    vm.getSystemProperties();
}

結果

BenchmarkModeCntScoreErrorUnits
JmxBenchmark.no1_vmAttachthrpt10653.834± 108.790ops/s
JmxBenchmark.no2_vmCachedGetPropertiesthrpt102330.529± 270.268ops/s

アタッチありは1.5msec程度、キャッシュした場合は0.4msec程度。

ということで、アタッチに掛かる時間は1msec程度と推定。割とでかい。


JMXConnectorFactory.connectのパフォーマンス

続いて、VMに対するJMX接続を行う際のパフォーマンス。


No3 : JMXConnectorの取得処理と、クローズ処理をする

No4 : キャッシュしていたJMXConnectorを使って、MBeanServerConnectionの取得とMbean情報を取得する

public JMXConnector no3_vmCachedGetConnector() throws Exception {
    String connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress");

    if (connectorAddress == null) {
        String agent = vm.getSystemProperties().getProperty("java.home") + File.separator + "lib" + File.separator + "management-agent.jar";
        vm.loadAgent(agent);
        connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress");
    }

    JMXServiceURL serviceURL = new JMXServiceURL(connectorAddress);
    JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceURL);
    jmxConnector.close();
}

@Benchmark
public void no4_connectorCachedGetMBeanCount() throws Exception {
    MBeanServerConnection connection = connector.getMBeanServerConnection();
    connection.getMBeanCount();
}

結果

BenchmarkModeCntScoreErrorUnits
JmxBenchmark.no3_vmCachedGetConnectorthrpt10612.652± 95.547ops/s
JmxBenchmark.no4_connectorCachedGetMBeanCountthrpt109047.803± 1480.741ops/s

JMXの接続と切断は1.6msec程度。おおまかVMに対するアタッチと同じぐらい。

接続したあとの、MBeanServerへの接続とMBean情報取得は0.1msecぐらいで、これは無視できる小さい。


ThreadMXBeanからThreadCountを取るパフォーマンス

今回の主目的はこれ。

ThreadMXBeanを使って情報を取るのと、

MBeanServerConnection.getAttributeで名前を指定して情報を取るのと、どっちが早いか。


No5 : MBeanServerConnection.getAttributeの名前指定でThreadCountを取得する

No6 : ThreadMXBeanを取得してから、getThreadCountで取得する

No7 : キャッシュしていたThreadMXBeanから、getThreadCountで取得する

@Benchmark
public void no5_connectorCachedThreadCount() throws Exception {
    MBeanServerConnection connection = connector.getMBeanServerConnection();
    Object count = connection.getAttribute(new ObjectName(ManagementFactory.THREAD_MXBEAN_NAME), "ThreadCount");
    sum += (Integer) count;
} 

@Benchmark
public void no6_connectorCachedGetThreadCount() throws Exception {
    MBeanServerConnection connection = connector.getMBeanServerConnection();
    ThreadMXBean threadBean = ManagementFactory.newPlatformMXBeanProxy(
            connection, ManagementFactory.THREAD_MXBEAN_NAME, ThreadMXBean.class);
    sum += threadBean.getThreadCount();
}

@Benchmark
public void no7_beanCachedGetThreadCount() throws Exception {
    sum += threadMXBean.getThreadCount();
}

結果

BenchmarkModeCntScoreErrorUnits
JmxBenchmark.no5_connectorCachedThreadCountthrpt108199.887± 1269.164ops/s
JmxBenchmark.no6_connectorCachedGetThreadCountthrpt102662.147± 585.627ops/s
JmxBenchmark.no7_beanCachedGetThreadCountthrpt108148.967± 1705.518ops/s

ThreadMXBeanを毎回取る(No6)は明らかにパフォーマンスが悪いけど、

ThreadMXBeanをキャッシュしている限りは、ThreadMXBeanから情報を取るのと、

MBeanServerConnection.getAttributeで取ることに性能差はなし。


まとめ

1. VirtualMachineへのattach/detachは時間が掛かるので、キャッシュすべき

2. JMX接続の確立/切断は時間が掛かるので、キャッシュすべき

3. MBeanServerへの接続は時間が掛からないので、無理にキャッシュしなくてよい(close処理もないのでリソース管理もしてない?)

4. MBeanServerConnection.getAttributeでもThreadMXBeanを使っても性能差はないので、ThreadMXBeanを無理にキャッシュしなくてよい


ベンチマークソースコード

https://github.com/cero-t/Benchmarks/blob/master/src/main/java/ninja/cero/benchmark/JmxBenchmark.java


現場からは以上です。

2015-10-19

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

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

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

好きなAWSサービスはKinesisです。まだ使ってませんけどね!


さて、今日のテーマは「AWSでTomcatのセッション共有」です。

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


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

2013-10-02

[]俺様とJavaOne 2013(後編)

いよいよJavaOne最終日です。

最終日はCommunity Keynoteと、いくつかのセッションを行なうだけで

夕方ぐらいには閉幕してしまいます。


Day 5 : 子供のプログラミング、どうしていますか?

Community Keynote

最終日、最初のセッションはコミュニティキーノート、

かつては、Gosling's Toy Showなどが行なわれていたイベントですね。

このキーノートで日本の皆さんにお伝えしたい事は、ただ一つ。


#てらだよしお が、Tシャツを投げていました!


あぁ、写真とか撮ってないです、すみません。

たぶん誰か撮ってます、そっちに期待してください。


私の席からは見えませんでしたが、James GoslingもTシャツを投げていたそうなので

この瞬間、てらだよしおはJames Goslingに匹敵するエンジニアだった、ということですね!


それはさておき、

このコミュニティキーノートで一番印象に残ったのは、教育の話です。

10〜14歳の子供を対象にしたDevoxx 4 kidsというイベントの様子が紹介され、

子供が「やった、動いた!」などと言いながら、プログラミングを楽しんでいました。


また、会場にはArun Guptaの10歳の息子、Aditya Guptaが登場して、

Minecraftというゲームをハックした時の考え方などを紹介しました。

「これはEclipseという開発環境で」「左側のソースファイルの一覧があって」などと

10歳の子供が説明するだけで会場は大盛り上がりなのですが、

恐らく、彼自身はなぜ会場が盛り上がっているのか分からなかったに違いありませんw


10歳の子供が、数千人(?)の大人を相手に、

大して緊張する様子もなく、声を震わすこともなく、説明しきった様子は圧巻でした。

会場のスタンディングオベーションも、決して過大評価ではなかったと思います。

本当に素晴らしかったです。


Devoxx 4 kidsの動画や、Aditya Guptaの発表を見るにつけ、

同世代の子供を持つ親としては、そろそろ子供に楽しいプログラミング

教えてもいいかな、という気持ちになってきましたね。


私自身、プログラミングを始めたのは、この年代だったと記憶しています。

一つの素養として身につけるには、決して早くない時期でしょう。


今度、科学未来館に行って、プログラミングできるおもちゃを買ってくるかな。


[CON4695] Java Memory Hogs

OracleのNathan Reynoldsによるセッション。

Keynoteが終われば帰る人も多い中、部屋は満席で立ち見が出るほどでした。

やっぱりメモリ系やGC系セッションは人気があります。


本当はこの前にいくつかのセッションを入れていたのですが、

Community Keynoteの後にJames Goslingとの記念写真撮影に並んでいるうちに

予約していたセッションが満席になってしまったり、

ランチで少し遠目に出ているうちに逃してしまったりなどして、

最終日のセッションはこれ一本になりました。てへぺろ。


さて、

このセッションのテーマはJOverflowを使ったヒープメモリの診断です。

というかJOverflowって、ただのヒープダンプを解析できるだけではなくて、

無駄なヒープの使い方を指摘する機能まであるんですね。いいですねこれ。


このセッションでは、実際のJavaEEアプリケーションのヒープダンプを取り、

それをJOverflowで診断して検出した問題について、改善方法や結果が示されました。


たとえば英数字しか入らないStringをcharではなくbyteで保持したら

7%ぐらいメモリ効率が上がったとか

同じ文字列のStringがたくさん重複していたからString.intern()で改善したとか

空の配列やCollectionがたくさんあったからLazy初期化するようにしたとか

そういう話です。


このセッションを通して感じたことは

「JOverflowは、ヒープダンプのFindBugs」だということですね。


たとえば空のCollectionがたくさんあるとか、

同じ文字列のStringの重複がたくさんあることなどは、

理論上は「減らせば良い」わけですが、Lazy初期化やinternを使った処理を書くとなると

パフォーマンスへの影響や、可読性の低下(処理の複雑化)は多少なりあるため

なかなか全ての場所で、理論通りにコーディングするわけにもいかないと思います。


その点、JOverflowを使い、実際に動かしたアプリケーションのヒープダンプを解析することで

「本当に問題になるところ」が分かるため、実際的に改善すべきポイントを掴めるわけです。

このアプローチ、まさにENdoSnipeと同じですね!(←これが言いたかったの?)


大事なことなのでもう一度言いますが、

「JOverflowは、ヒープダンプのFindBugs」です。

ぜひ試してみましょう。


JavaOne 2013最後のセッションでしたが、かなり面白い内容でした。


JavaOneを終えて、最後に。

そんなわけで、これでJavaOne 2013は終わりです。


今年のJavaOneで一番印象的だったことは、

もちろん私自身が初めてスピーカーを務めたことですが、それを除けば、

子供へのプログラミング教育の話でした。


同世代の子を持つ私としては、

Devoxx 4 kidや、10歳のAditya GuptaによるCommunity Keynote発表などは

非常に印象的でワクワクすると同時に、負けていられないという気持ちになりました。


一方で、Javaも転換期に来たのかな、という印象もありました。


少し話はそれますが、

先日、「教育」に関する有名ブロガーのつぶやきが話題になりました。

人が教育の話をしはじめるのは、自分の成長に限界が見えた時、

要するに「自分の成長よりも、後進の成長のほうが価値がある」と考えるようになった時だ、

というアレです。


この話も踏まえると、このJavaOneで「子供の教育」の話がなされるというのは

多くのJavaの開発者たちが、あるいは、Javaそのものが転換期に来た、

悪く言えば「焼きが回った」ところにさしかかったのかな、という印象になったわけです。

いや、たぶんに感覚的な話なんですけどね。


また、今回は新しい情報がほとんどないJavaOneでしたが、

個人的に(弊社的に)言えば、Flight RecorderやMission Controlの

裏側の仕組みや、今後目指すところを聞いたことで、

ENdoSnipeの開発も改めて頑張ろうという気になりました。


ENdoSnipeを、Duke's Choice Awardをとれるぐらいの秀逸なプロダクトにしたいなと、

そんな大きな野望を持ちながら帰国の途につきました。


よし、頑張ろう!

2013-09-30

[]俺様とJavaOne 2013(中編)

JavaOne 3日目、自分のセッション当日は

やっぱり直前まで資料準備&練習でバタバタしていました。

この性格、死ぬまで治らない予感!


Day 3 : 解析ツールのセッションは人気

3日目、僕のセッションは夜7時半からなので、

朝イチにあった面白そうなセッションに参加していました。


[CON5092] Diagnosing Your Application on the JVM

元BEAのStaffan Larsenのセッション。

朝イチにも関わらず、満席になる人気セッションでした。


内容は、診断・解析ツールについて、デモを交えて次々と紹介するセッションで

主に7u40から使えるようになった(7u4から使えてたものもあるけど)

「jcmd」(旧jrcmd)を中心に紹介されていました。


ちょっと列挙しますと・・・

jps : Javaプロセスの一覧を列挙する

jcmd : 引数なしならjpsと同じ

jcmd <pid> VM.uptime : Javaプロセスの起動後の経過時間

jcmd <pid> Thread.print : スレッドダンプ。jstackと同じ。

jcmd <pid> GC.heap_dump : ヒープダンプ。jmapと同じ。

jcmd -gcnew <pid> 1s : 毎秒のGC領域のサイズを見る

jcmd <pid> PerfCounter.print : JVM内部で保持している様々なカウンタを取得

などなど。


自分的に衝撃だった事と言えば、jcmdコマンドの引数で pid に 0 を指定すると

全てのJavaプロセスの情報をまとめて取れる、というところ。

実際に使う機会があるかどうかは分かりませんが、良い事を知った感がありますね(笑


またセッション後半は、このような解析ツールをリモートから実行するために、

jstatdやJMX Remoteを利用するという話や、その裏側の仕組みが説明されました。


正直、他の作業をしながら聞いていたので、かなり聞き逃してしまったのですが

リモートから診断・解析をする時に、何ができるか・できないかを判断するための

背景となる知識が得られる良い内容でした。まさにJavaOneらしい内容だったと言えます。


特に解析する機会が多いとか、開発ツールを作る立場であるとか、

そういう人は、このセッションの資料をきちんと読むべきだと思いますね、

っていうか、私も、きちんと読み直します!


ところで、セッションの後に

こんな風にスピーカーのStaffanさんに、お礼なのか挑発なのか分からないツイートをして

私のセッションに来て頂きました。我ながら、強引なことをしたもんです。


そんなわけで、夜には自分のセッションがあったわけですが

それについては、前後の話も含めて、またきちんと別エントリとして投稿します。


Day 4 - Lambda、Lambda、JFR

おはよう世界。

自分のセッションが終わった開放感からか、倒れ込むように寝てしまい

これは昼まで寝るかなと思ったら、意外と5時間睡眠ぐらいで目が覚めてしまい

時差ボケの威力を実感した早朝でした。


そんなわけで、朝から元気にセッションに参加します。

そう、僕のJavaOne参加はここから始まったわけです。


[CON2055] Programming with Lambda Expressions in Java

Agile Developer, Inc.の社長、Venkat Subramaniamによる

軽妙でウィットに溢れたLambdaのセッションでした。


内容的には、外部イテレーターから内部イテレーターの書き方の移り変わり、

Lambdaの文法やstreamの使い方や効果などを紹介するという

比較的、初級者向けのセッションなのですが、その語り口調が面白すぎて

本当に笑いの絶えないセッションでした。


直接的な表現よりも、間接的な表現を軽妙に語る事で面白さを増す感じでしたね、たとえば

  • 汚い → とても子供に見せられない。しっ見ちゃいけません!
  • 危険 → 何をやろうとしているんだ、家に帰って考え直せ!
  • 素敵 → これは食欲をそそる!

などなど。

って私はJavaOneに来て何を学んでるんですかね。


もう少し実用的なところをフィードバックすると、

やはりLambda時代にはAPIデザインが少し変わるということでしょうか。


たとえば自分で比較するユーティリティメソッドを書く際には、

isPriceLessThan(500, value) と書けるようなAPIを提供するのではなく、

isPriceLessThan(500).test(value) と書けるようなAPIを提供することで

Lambda式として利用できるようにしていました。


ちょっとこの辺り、自分でも消化しきれていないので

日本に帰ったら資料を見ながら復習しようと思います。


ってよく考えたら、このセッション、

テキストエディタだけで話してたから、資料ないんだった (^^;;


[CON7942] Java 8 Streams: Lambda in Top Gear

続けてのLambdaセッション。

Paul Sandozと、Lambdaの神Brian Goetzのセッションです。


streamのAPIは、集計処理などにおいて、

うまくparallel化ができるもの、できないものがあったり、

処理を途中で中断しても良いもの、全ての要素を走査するものがあるなど、

APIは、いくつかのカテゴリで「分類」することができます。


この分類次第で、parallel化した時のパフォーマンスなども変わってくるため

streamを使う際には、この分類をきちんと押さえておかなければいけない、

ということが説明されていました。ちょっと自分にはなかった視点でした。


この辺りは、資料をダウンロードして学び直す必要があるので、

ボロが出ないうちに、説明をこの辺で切り上げましょう (^^;


[CON5091] Java Flight Recorder Behind the Scenes

3日の朝イチに解析ツールの紹介していた、Staffan Larsenのセッション。

前半こそFlight Recorderの紹介だったのですが、

後半はFlight Recorderの設計の話が展開され、かなり興味深かったですね。

そんな後半の話だけピックアップして紹介します。


1. Thread buffers

Flight Recorderが取得した情報は、スレッドローカルのThread buffersに貯めてから、

共有のGlobal buffersに書き出します。

こうすることで、Global buffersへの書き込みが衝突することを抑えています。

この辺りは、Flight Recorderがメイン処理に影響を与えないようにするために

欠かせない、いわば当たり前の設計でしょう。


2. Flight Recorderは永久には情報を取り続けない。

メモリリークへの対策として、情報は一定期間かサイズごとに消すか上書きしています。

これも当たり前のことですが、私は過去にちょっとやらかした事があります (^^;


3. Flight Recorderは、アプリケーションのクラスやオブジェクトへの参照を持たない。

これも、メモリリークを防ぐうえで不可欠のポリシーです。


4. クラス名はIDに変換する。

クラス名(文字列)を、int程度の数値に変換することで、

ファイルやメモリの空間効率を向上させます。

また、そのクラス名と数値のマップをFlight Recorderの出力ファイルに

持たせておくことで、互換性や移植性にも配慮しています。

こういう細かいところも、きちんと工夫しているんですね。


5. クラス一覧自体も定期的にリセットする。

クラス一覧がメモリリークの原因にならないよう、

一定期間ごとにファイルに出力して、クラス一覧をクリアしてしまいます。

(このタイミングを「チェックポイント」と読んでいました)

最後にクラス一覧をマージするかどうかは、ちょっと分かりませんでした。

マージしないと重複が出てしまって、ファイル効率がよくない気がします。


6. スタックトレースのpoolを作る。

同じスタックトレースが何度も表れることが多いため、

スタックトレースのプールを作っておいて、(全く)同じスタックトレースが

発生した場合には、前のスタックトレースへの参照を使うだけにします。

なるほど、勉強になります。


というような、ENdoSnipeの開発者的にありがたい情報がたくさんありました。

もちろんアプリケーション開発をするうえでも、このようなメモリやデータの

効率化の仕組みを「発想」することは、とても大切だと思います。

まだまだやるべき事があるのだなと、改めて思い知らされた感じです。


[CON2959] Modular JavaScript

Luminis Technologies社のSander Mak、Paul Bakkerのセッション。

なんかJavaとの連携もありそうな感じのアジェンダが提示されていたんですが

実際には、JavaScriptのライブラリやフレームワークを使ったときの

packageやclassの可視性なんかを、延々延々とJavaScriptのソースで説明するセッションで、

Javaコードは全く出てきませんでした。


ここJavaOneやぞ!


4日目終わり

この後の時間帯に、kotlinのセッションがあったり、

あの #てらだよしお さんのJavaEEのセッションがあったのですが、

どうしても眠かったため、ホテルに戻って休んでいました。


聞くところによると、kotlinのセッションは10人いなかったそうです。

kotlinの過疎感ハンパない!(><)


そして寺田さんのセッションは「質問はTwitterでお願い」と言っていたにも関わらず、

バンバン質問が出て、大変だったそうです(そして、きちんと回答したそうです!)

そりゃOracleのエンジニアがJavaEEについて話したら、質問出るって!


ちなみに4日目の夜には、トレジャーアイランドで

Maroon5というバンドのライブなどあったのですが、

上にも書いた通り、ホテルに戻ってお休みしていました。


Folder5が来るんだったら、無理してでも行ったと思うんですけどね。

#行かねーよ。


そんなわけで、JavaOneも後半戦に差し掛かってきました。