« 2012年7月 | メイン | 2012年9月 »

2012年8月

2012年8月30日 (木)

Playframeworkでガラケーサイト

こんにちは。

今日はPlayframeworkでガラケーサイトを作成した話をしてみたいと思います。
ちなみにガラケー対応をやったのは人生初です。(^^;;;

そんなに凝ったページではなかったんですが、ガラケー界のしきたり(?)とPlayの思想がそぐわない部分があってそこで多少面倒があったのでその部分の情報共有です。(読み直したら愚痴みたいになってますけど。。。(--)

 

★charsetはShift_JIS

知っている人からすれば当たり前の話なんでしょうがガラケー界では今もWebページはShift_JISで作成するのが常識のようです。

実際試してみると手元で試した端末はすべてUTF-8のページも表示できましたが、古い端末は化けると聞けばそこで冒険するのはまだ危険かと思います。

ちなみに世の慣習にならって「Shift_JIS」と言っていますが実際にはガラケーの文字コードはMS932+絵文字です。(多分)
絵文字はともかく丸数字のようなMS932固有の文字は対応しておかないとやっぱり文字化けしてると言われるんでしょう。。。

 

★PlayでShift_JISページを返す

Playframeworkは基本文字コードはUTF-8で統一というスタンスで作られています。
なので何も考えずに作るとすべてのレスポンスはUTF-8で返されます。

では別の文字コードでページを返すためにはどうすれば良いかというとこれはrenderメソッドでページを返す前にresponseにencodingをセットするだけです。

 

/** Webフォームを含むページを返す */
public static void form() {
	response.encoding = "Shift_JIS";
	render();
}

 

ちなみにこの場合でもテンプレート自体はUTF-8で記述します。

一般的なブラウザは次のリクエストの送信をレスポンスページと同じ文字コードで実行するので理屈の上では対応はこれだけで良いはずです。
が、実際にはこのフォームから受けたパラメータの日本語は全部化けてしまいます。

 

★Httpリクエストではcharsetがつかないのが普通

ガラケーに限った話ではありませんが、ほとんどのブラウザはリクエスト送信時にContent-Typeのcharset属性を付加しません。

Playの実装ではContent-Typeのcharsetがある場合はそれを用いてリクエストをパースしますが、ない場合はUTF-8でパースします。
要するにShift_JISの文字をUTF-8としてパースしようとするから化けてるんです。

 

★どこでリクエストのエンコーディングを指定するのか?

さて、となるとどうにかしてリクエストのエンコーディングを明示しなければならないのですがAPI自体は単純です。
Requestオブジェクトのencodingに値を設定すれば良いだけなので。

ただし、いつ設定するかという問題はまだ残っています。当然ながらリクエストのパースが終わった後にエンコーディングを変更しても意味がないので、例えば以下のコードはアウトです。

 

/** リクエストから生成した結果ページを返す */
public static void result(String name) {
	requet.encoding = "Shift_JIS";
	response.encoding = "Shift_JIS";
	render(name);
}

 

何故ならこのresultメソッドを呼び出すためにはリクエストをパースしてパラメータからnameの値を取り出さなければならないからです。

ちなみにPOSTの場合に限り以下のコードはOKです。

 

/** リクエストから生成した結果ページを返す */
public static void result() {
	requet.encoding = "Shift_JIS";
	String name = params.get("name");
	response.encoding = "Shift_JIS";
	render(name);
}

 

何故ならこの場合resultメソッドに引数がないため、リクエスト(のボディ)をメソッド実行前にパースする必要がないからです。この場合リクエストボディのパースはParams#getメソッドの実行まで遅延されます。

ただしGETのクエリ文字列のパースは常にコントローラのメソッド呼び出しの前に行われます。そのためこのアプローチではどうやってもGETパラメータは化けるので、フォームからデータを送信する際には常にPOSTで送信する必要があります。(PlayPluginを作れば対応できそうですけど未検証です。)

 

★@Beforeを使う

先の2つ目の方法で一応正しいエンコーディングでリクエストをパースすることはできるようになったわけですが、コントローラのメソッドで引数を使うことができないというのは結構いたい制約です。また、すべてのメソッドでrequestとresponseのエンコーディングを設定しなければならないのも面倒ですしバグのもとです。

なのでこれらの設定は@Beforeインターセプタに移動してしまう方がお勧めです。

 

/** エンコーディングをShift_JISに設定 */
@Before
public static void convertEncoding() {
	requet.encoding = "Shift_JIS";
	response.encoding = "Shift_JIS";
}

 

@Beforeはリクエストのパース前に実行されるのでこれでPOSTには対応できます。
(GETのクエリ文字列にはやはり対応できません。)

ここでは強制的にすべてのメソッドのエンコーディングをShift_JISに変更しているのでPC用のサイトなどエンコーディングをUTF-8で扱いたいページが混在する場合はコントローラ自体を分けてガラケー専用にします。

 

★「あめりか」は化けないけど「アメリカ」は化ける

さて、ここまでの設定で一見文字化けは解消されたかに見えます。が実はまだ文字化けは発生します。
それも「あめりか」は化けないけど「アメリカ」だと「ア」と「カ」だけが化けるという理不尽な化け方をします。

これは。。。実はPlayの問題というよりはJavaのURLDecoderの問題です。
昔から知ってましたが、もうとっくに直ったかと思ってました。。。(--

 

★「あめりか」と「アメリカ」はどこが違う?

前者はひらがなで後者はカタカナですね。(^^;
ちなみにShift_JISでのコードポイントは以下になります。

 

あめりか = 82 A0 - 82 DF - 82 E8 - 82 A9
アメリカ = 83 41 - 83 81 - 83 8A - 83 4A

 

ここまで書くともう気がついた人もいるかと思いますが、「ア」と「カ」はShift_JISの2バイト目がASC-IIの範囲と重なっています。

 

★JavaのURLDecoderとブラウザのURLエンコード

URLエンコードの仕様についてはとりあえずウィキペディアでも参照してください。

これを読むとShift_JISの2バイト目にASC-IIが表れた場合の扱いはエンコードしてもしなくても良さそうなので以下の二つは両方とも正しいURLエンコードです。

 

★全部エンコード
%83%41%83%81%83%8A%83%8A%83%4A

★Shift_JISの2バイト目がASC-II範囲の場合はエンコードしない(0x41=A, 0x4A=J)
%83A%83%81%83%8A%83%8A%83J

 

問題はブラウザのURLエンコードが後者の形式であるのに対し、Javaの標準のURLDecoderがその形式に対応していないことにあります。

ちなみにJavaのURLEncoderは前者の形式の文字列を返し、それはJavaのURLDecoderで正しくデコードできます。このことから、これはバグではなく仕様だという主張もあるようですが。。。個人的にはそれには納得できません。

その理由としては、まず第1に実際に後者のエンコードを行う処理系が世の中(少なくとも日本)には多数存在するということをあげますが、それ以上に思うのはこれを修正したところで何の不都合もないということです。
原理的にこの修正を行っても何らかの非互換の発生する文字コード体系は世界中のどこにも存在しないと思いますが、そんなことないんですかね???

実装を考えても素直に実装すれば対応する方が楽なはずで、今のJavaの実装の方が効率が悪いと思うので対応しない理由がわかりません。

 

★URLエンコードの形式を変換する

なんか激しく話が脱線しましたが、とにかくこれはどうにかしなければなりません。
対応方法は色々あると思いますが今回は時間も限られていたので安易にパース前のボディを一度自力でデコードしてJavaのURLEncoderでエンコードしなおすという方法を取りました。後者の形式に対応したURLDecoderとしてはcommons-codecのURLCodecがPlayのlibに最初から入っています。

 

@Before
static void convertEncoding() {
	request.encoding = "Shift_JIS";
	response.encoding = "Shift_JIS";
	request.body = convertStream(request.body);
}

private static InputStream convertStream(InputStream is) {
	final String encoding = "Shift_JIS";
	
	byte[] data = null;
	try {
		byte[] byteBuf = new byte[1024];
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		int n = is.read(byteBuf, 0, byteBuf.length);
		while (n > 0) {
			bos.write(byteBuf, 0, n);
			n = is.read(byteBuf, 0, byteBuf.length);
		}
		data = bos.toByteArray();
		if (data.length > 0) {
			URLCodec codec = new URLCodec();
			String str = new String(data, "us-ascii");
			StringBuilder buf = new StringBuilder();
			String[] values = str.split("&");
			for (int i=0; i<values.length; i++) {
				String value = values[i];
				int idx = value.indexOf("=");
				if (idx == -1) {
					value = codec.decode(value, encoding);
					buf.append(URLEncoder.encode(value, encoding));
				} else {
					String key = value.substring(0, idx);
					value = value.substring(idx+1);
					key = codec.decode(key, encoding);
					value = codec.decode(value, encoding);
					buf.append(URLEncoder.encode(key, encoding))
						.append("=")
						.append(URLEncoder.encode(value, encoding));
				}
				if (i<values.length - 1) {
					buf.append("&");
				}
			}
			data = buf.toString().getBytes("us-ascii");
		}
	} catch (DecoderException e) {
		data = new byte[0];
	} catch (IOException e) {
		data = new byte[0];
	}
	return new ByteArrayInputStream(data);
}

 

めっちゃ無駄ですけどね。(--

ちなみにPlay本体を修正するのであればUrlEncodedParserでURLDecoderを使用しているところをURLCodecに替えるだけで直るはずです。

 

★Shift_JISはMS932として扱う

最後にMS932対応について。

JavaではShift_JISとMS932は厳密に区別されているので今のままでは丸数字などのShift_JISにはなくてMS932にだけある文字は化けます。

かといってcharsetとして「MS932」とか「Windows-31J」とか書いて返してもガラケーが正しく解釈してくれるかどうかはまったくもって怪しいのでコードで対応するのはかなり苦しいです。

なのでこれは起動時の引数でcharsetのエイリアスを差し替えてしまうのが一番楽だと思います。

 

play run -Dsun.nio.cs.map=Windows-31J/Shift_JIS

 

こうすると「Shift_JIS」というcharset名が表れた場合にJavaの方でそれをMS932として扱ってくれます。
当然厳密な意味での「Shift_JIS」という文字コードは正しく扱えなくなるわけですが多分それで困ることはありません。(^^;;;

思った以上に長くなって最後若干投げやりでしたがちょっとは役に立つエントリになったでしょうか?(^^;
心の底からUTF-8だけでコンテンツ作れる世の中に早くなってほしいです。

2012年8月 2日 (木)

Playframeworkとスレッド数

Playframeworkを少し大きめのスレッド数(50)で動かしていたら、自分の予想よりも大きくメモリを消費しました。
今回はその調査レポートです。
なお対象としたPlayのバージョンは1.2.5です。

Playのスレッド数のデフォルト値

まずPlayのスレッド数の設定について少し考察しておきます。

Playのスレッド数はapplication.confの「play.pool」というキーで設定されます。
このキーが省略された場合のデフォルト値は以下のようになります。

  • DEVモード - 1
  • PRODモード - 環境で利用可能なCPU数 + 1

DEVモードとPRODモードは開発環境と本番環境で設定を変更するためのフラグです。

このように開発時と本番で異なるデフォルト値が適用されますが、個人的にはこれらのデフォルト値はDEVモード、PRODモードどちらの場合においても適切ではないと考えています。
以下にそう考える理由を説明していきます。

DEVモードのデフォルト値「1」

スレッド数が1の場合同時に処理できるリクエスト数はひとつだけになります。つまりあるリクエストが実行中の間は他のリクエストは開始されずに待たされることになります。

このことは以下のようなリクエストハンドラを作成することで容易に確認できます。

    public static void ok() {
        System.out.println("ok");
        renderText("ok");
    }
    
    public static void sleep() {
        System.out.println("sleep start");
        try {
            Thread.sleep(60 * 1000);//1minutes
        } catch (InterruptedException e) {
        }
        System.out.println("sleep end");
        renderText("sleep");
    }

okに対するリクエストは通常は即座に返ってきますが、sleepが実行中の場合はその終了まで待たされます。

このように全てのリクエストをシリアライズしてひとつずつ実行する方式にはデバッグを容易にするというメリットがあります。しかし実際のWebアプリケーションでは常に複数のリクエストが同時並行的にやってきます。
複数のユーザーが同時にリクエストを発行する可能性があるのはもちろんですが、現代のWebアプリケーションでは一人のユーザーが開いているひとつの画面からAjaxによって複数のリクエストが同時に発行されることもままあります。

このことから本番環境がマルチスレッドで実行されるアプリケーションであるならば、開発環境がシングルスレッドあることのメリットはほとんどないと思います。逆に複数のリクエストで共通に使用するリソースのデッドロックが発生するなど開発環境では発生しない問題が本番環境だけで発生する可能性もあるので、原則的には開発環境であっても常にスレッド数はある程度大きな数字を設定しておくべきと考えています。(もちろんデバッグ目的で一時的にスレッド数を1にすることは問題ありません。)

ちなみにここでのスレッド数の設定はリクエストハンドラだけでなく静的なコンテンツにも適用されるので、Playのpublicフォルダから提供するイメージを多数含むようなページを返す場合にもパフォーマンス的に不利です。

PRODモードのデフォルト値「CPU数+1」

個人的な感想ですがPlayには非同期IOに対して過剰な期待があるように感じられます。
「IOをすべて非同期化すればCPU効率は最大化する」という理想があってそれを実現すべく実装が行われているような印象を受けるのです。(これはどこかに書かれているのを読んだことがあるのではなくて僕が勝手にそう思っているだけです。)

そうした理想を実現したシステムでは「CPU数+1」という設定値は確かに最適な値なのかもしれません。しかし実際にはすべてのIOを非同期で実現したシステムを作るのはほとんど不可能ではないかと思うのです。

例えばSalesforceやEvernoteなどの外部サービスを実行する場合、それらのサービスプロバイダーが提供するSOAPやRESTのWebAPIを叩きますが、それらはすべて同期IOです。
またJDBCを使用したデータベースアクセスも同期IOなので、そう考えると同期IOの全くない処理というのはほとんどないように思います。

そして同期IOがあるのであれば、必ずそこにはIOウェイトが発生するはずで、その間に他のスレッドが処理を進めることができるのであればその方が効率が良いです。

なのでPRODモードのスレッド数もデフォルト値をそのまま使用せずに環境や想定ユーザー数によって調整することが必要と考えています。

スレッド数を決める際に考慮する事項

こう書くと次にはそれでは最適なスレッド数はいくつなのか?という話になると思いますが、これはアプリケーションの処理内容や環境によって色々な要素が絡み合うので一概には言えません。
ざっと考えただけでも以下のような要素を考慮する必要があると思います。

  • CPU数 - CPU数に比してスレッド数があまりに多いとスレッドのスイッチが頻繁に発生することによって効率が低下する
  • メモリ - 同時並行的に処理するスレッドがそれぞれ、そこそこメモリを消費するものであるならばスレッド数が多ければそれだけ多くのメモリを消費する
  • 想定ユーザー数 - 同時アクセスが予想されるユーザー数よりも極端にスレッドが多くても無駄なだけだし、少なければ待ち行列が発生する
  • DBコネクション数 - DBの接続数が制限された環境の場合、接続可能なコネクション数を超えてスレッド数を増やしてもDBアクセスで結局待ち行列に入ることになる

などなど。

まぁ実際のところはこれらを厳密に計算するようなことはなくて20とか50とかに直観的に決めてますけど。(^^;;;
チューニングのやり方は色々あると思いますが、僕の場合は最初に大きめの値を設定して限界を超えていそうな要素があれば、そこを睨みつつ数値を調整するというやり方をとります。

Playでスレッド数を大きくした場合の注意点

さて前振りが長くなりましたが、今回スレッド数を50とした状態である程度メモリを消費すると思われる処理(=POIによるExcelファイルのパース)を繰り返し実行してみたところ実行毎に使用メモリが徐々に大きくなるという現象が確認できました。

最初はgcが実行されていないだけかと思いましたが、gcを実行してもほとんどメモリは解放されません。まさかメモリリークかよ!と憂鬱な気持ちでVisualVMで調査したところやはり使用済みの巨大オブジェクト(=パース後のExcelワークブック)がメモリに残り続けてました。(--

本気でメモリリークかと一瞬あせりましたが、よくよく調べてみると原因はThreadLocalからの参照でした。

リクエストハンドラでテンプレートを使用する場合renderメソッドに任意のオブジェクトを引数として渡せますが、ここで使用したオブジェクトはリクエストを抜けても次回スレッドが再利用するまでは参照が残り続けるようです。

スレッド数が50の場合、おそらく各スレッドがサイクリックに使いまわされると思うので最大50回分の引数オブジェクトがメモリに残ります。

今回の場合巨大オブジェクトを参照するメソッドばかりを繰り返し実行したのでメモリ消費が極端になったわけです。実際にはほとんどのメソッドは文字列や小さめのリストしか参照しないのでここまで極端な結果にはならないと思いますが、できればこれはフレームワーク側でリクエストを抜けた時に参照をクリアしてほしいですね。

採用情報

株式会社フレクトでは、事業拡大のため、
Salesforce/Force.comのアプリケーション
開発
HerokuやAWSなどのクラウドプラッ
トフォーム上でのWebアプリケーション開発

エンジニア、マネージャーを募集中です。

未経験でも、これからクラウドをやってみた
い方、是非ご応募下さい。

フレクト採用ページへ

会社紹介

株式会社フレクトは、
認定コンサルタント
認定上級デベロッパー
認定デベロッパー
が在籍している、
セールスフォースパートナーです。
heroku partnersにも登録されています。
herokuパートナー
株式会社フレクトのSalesforce/Force.com
導入支援サービス
弊社の認定プロフェッショナルが支援致します。
・Visualforce/Apexによるアプリ開発
・Salesforceと連携するWebアプリ開発
も承っております。
セールスフォースご検討の際は、
お気軽にお問合せください。
Powered by Six Apart