playframework

2013年5月24日 (金)

HerokuにPlay2アプリをpushする際にキャッシュをクリアする

こんにちは

最近GitHub上に自前のMaven-Repositoryを作成しました。

僕は普段の仕事では汎用のライブラリ(Java)を書く時間とWebアプリ(Play1 or Play2)を書く時間が半々くらいなんですが、これまではアプリから自前のライブラリを使用する場合は作成したjarファイルを直接アプリのlibフォルダにコピーしていました。

それがMaven-Repositoryをインターネット上に置くことによってアプリのライブラリ管理フレームワーク(Play1ではivy、Play2ではSBT)から直接取得することができるようになるわけです。

作成したアプリはたいていHerokuで動かしているんですが、Play2の場合はBuild.scalaにリポジトリの情報を設定することで自前のリポジトリからもライブラリを取得することができます。

残念ながらPlay1の場合はHerokuから自前のリポジトリを使うことはできません。Play1の枠組みではリポジトリの設定は「ivysetting.xml」で行うのですが、Herokuのbuildpackが内部でこのファイルを上書きしてしまうためです。(自分でカスタムbuildpackを作れば対応は可能なはずですが。)

 

★Play2で設定してみる

せっかくリポジトリを作成したので現在作成中のアプリでjarをリポジトリから取得するように修正してみます。

修正するファイルはBuild.scalaの1ファイルのみです。

 

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName         = "sqlsync"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    jdbc,
    anorm,
    "postgresql" % "postgresql" % "9.1-901.jdbc4",
    "jp.co.flect" % "flectSalesforce" % "1.0-SNAPSHOT"
  )


  val main = play.Project(appName, appVersion, appDependencies).settings(
    // Add your own project settings here      
    resolvers += "FLECT Repository" at "http://shunjikonishi.github.io/maven-repo/"
  )
}

 

こんな感じ。

元々はlibフォルダにflectSalesforceとそれの依存するflectCommon, flectSoapの各jarを置いていたんですけど、それらをまるっと削除して起動してみます。

すると起動時にSBTによる依存性解決のログがずらずらと流れて無事に起動。ちゃんと動きます。

次いでHerokuにpushしてみると、Slugコンパイル時に依存性が解決されてこちらも無事に動作しました。

素晴らしい。

 

★ライブラリ開発中はこんな設定するもんじゃない

と、喜んではみたものの実は現在の開発はライブラリの開発が主でアプリは単純にそれにUIをかぶせているだけのものだったのでした。。。

つまり、これまでは日常的に以下のような操作を行っていました。

  1. エディタでライブラリのコードを修正
  2. Mavenでビルド
  3. Antで生成したjarファイルをアプリのlibフォルダにコピー
  4. アプリ再起動して動作確認

めんどくさい。(--
(ちなみにIDEの類は普段まったく使いません。)

これがリポジトリを使用するようになると以下のように手順が変わります。

  1. エディタでライブラリのコードを修正
  2. Mavenでビルド
  3. MavenでGitHubのリポジトリにデプロイ
  4. play cleanコマンド実行
  5. アプリ再起動して動作確認

「play clean」が必要なのはそうしないとSBTのキャッシュが効いて更新されたjarファイルがリポジトリから取得されないためです。

そもそも手順が増えてるし、GitHubへのデプロイはかなり時間がかかるし、キャッシュがなくなっているせいで起動時に毎回依存性の全解決が走るし、とまったく良いことがありません。。。(--

ていうか、コード1行修正しただけでもデプロイが必要とか明らかに間違ってますよね。

ライブラリの安定ビルドを使用するだけのアプリならリポジトリから持ってくれば良いですが、今回のようにライブラリ開発とアプリ開発がセットになっているケースでは、これまで通りjarを直接libにコピーする方式を取った方が良いように思います。

ここに至って初めてMavenがプロジェクト新規作成時に付加するバージョン番号「1.0-SNAPSHOT」の意図がちゃんとわかった気がします。(^^;;;

 

★HerokuのSBTキャッシュをクリアする

ところでこの話、まだ続きがあります。

ライブラリを何回か更新したところで、再びHerokuにpushしたんですがこれはコンパイルエラーになりました。

理由は追加したはずのメソッドが見つからないため。そう、Herokuのスラグコンパイル時にもSBTのキャッシュは有効なため新しいjarを持ってきてくれないんですよね。。。

今回の場合リポジトリを使う設定を外してlibコピー方式に変更すれば良いのですが、なんか方法があるだろうと思って探してみたところ見つけました。

http://stackoverflow.com/questions/15945263/heroku-play-framework-2-sbt-dependencies-cache

要約するとheroku configに「BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-scala.git#cleancache」を指定するだけ。そうすると次回ビルド時にはキャッシュクリアバージョンのbuildpackがスラグコンパイルしてくれるという寸法です。

実のところこの解決方法自体は予想通りのモノでした。探し始めた時点でbuildpackにキャッシュクリアする1行を足せばできるよなぁ、と思っていたので。

そんな中で今回の発見はこれ。

BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-scala.git#cleancache

buildpackは元々GitHubで公開されたものを直接持ってくるという大胆な仕組みで動いてますが、URLの末尾に「#xxxx」をつけるとmasterではなく指定のブランチを持ってくることができるのでした。

そう思ってみると、実はbuildpackのブランチには若干の機能違いと思われるブランチがいくつか並んでいたりもします。

いや~、知らなんだ。。。

残念ながらブランチの説明というのは(git的にも)ないので、どこが違うのかを見るためにはdiffを見るしかないですがGitHubのブランチはこういう使い方もアリなんですね。(^^;

2012年11月 5日 (月)

HerokuのWorker再起動問題を考える

HerokuのWorker再起動問題を考える

こんにちは。

9月にこんな記事を書きました。

JavaでHerokuのWorkerを作成する

Heroku上のWebアプリをPlayframeworkで作るなら、WorkerもPlayで作るのが楽だよね!という話だったんですが。。。
実はこれ、ちょっと微妙な罠がありました。(--

 

★すべてのDynoは1日に一回再起動する

Herokuを運用しているとわりとすぐに気が付くと思いますが、WebDynoはだいたい24時間サイクルで再起動しています。
このあたりの挙動は以下のドキュメントに記載されています。

https://devcenter.heroku.com/articles/dynos#automatic-restarts

1日に一回サイクルすると書いてありますね。
僕はWebDynoについてはこの挙動はアリだと思っています。
WebDynoが仕事中であるか否かはRouterは把握しているはずなので、Routerが適切にWebリクエストをせきとめてくれるなら少なくとも原理上はリクエストの取りこぼしは発生しませんし、耐障害性を考えてもこのアーキテクチャは優れていると思うからです。

(prebootが早く標準になってほしいというのはありますけど。気になる人は「heroku labs」と叩いてみましょう。(^^v)

しかしWorkerDynoまで同じサイクルで再起動するのはどうなんでしょうね???
ちなみに再起動の方法は以下のドキュメント

https://devcenter.heroku.com/articles/ps#graceful-shutdown-with-sigterm

最初にSIGTERMを送って10秒待っても終了しなかった場合はSIGKILLで殺すっていう感じですね。
Workerが仕事中であるかどうかは外部からは知りようがないので夜間バッチなどの長時間かかるタスクを実行中であっても問答無用で再起動がかかりそうです。。。(--

 

★Workerの動作を観察する

んで、実際にWorker( on Playframework1.2.5)を数日間動かしてみて以下のような観測結果を得ました。

  • 再起動間隔は約24時間だが厳密ではなくかなり前後する
  • 予想通り長時間のJobを実行中であっても問答無用で再起動はかかる
  • SIGTERMはRuntime#addShutdownHookで捕まえることができるが、フックしたところでできることがほとんどない

さてさて困ったものです。(--
再起動間隔はざっと見た感じ24時間よりも若干長い傾向があるので仮に最初に午前中にWorkerを開始したとしても時間の経過とともに、夜間バッチの時間帯に再起動がかかるということはありえます。
ていうか、そもそも「Workerは夜間バッチの時間から遠く離れた時間に起動すること」みたいな謎の運用ルールはアウトでしょう。

つまりここまでの結論としては、Workerは

  • 実行に長時間かかる中断不能な処理
  • 処理が途中で中断された場合にやり直しのきかない処理

を扱うには不適ということになってしまいます。。。
マジでかーーー!!!(--

 

★Schedulerを併用する

さて。気を取り直して、どうにかしてこの問題に対処しなければならないわけですが、最初に考えたのはSchedulerでherokuコマンドを実行するというものです。
Jobを動かす時に「heroku ps:scale worker=1」として不要になったら「heroku ps:scale worker=0」とするという作戦です。
この方法にはWorkerのdyno時間を節約できるというメリットもあります。

Schedulerで実行できるコマンドは「heroku run bash」で確認できるのでとりあえず試してみます。

 

>heroku run bash
Running `bash` attached to terminal... up, run.1
~ $ heroku ps
bash: heroku: command not found
~ $

 

あ~。。。(--
heroku環境にherokuコマンドはないようです。。。。
時間帯によるWebDynoの増減とか割と用途はある気がするんだけどなぁ。。???(--

ちなみにJavaのHerokuAPIを入れるとそれはちゃんと動きました。
ひょっとするとSSHの鍵から入れないといけないんじゃないかと憂鬱だったけどそれは必要ないらしい。(gitは入っているので、そこで必要なんだと思う。)

でもAPIキーをどっかから取ってきて、HerokuAPIを実行するプログラムを書いて、とちょっと面倒くさい。。。(--

□□□□

次にやったのはplayコマンドをworkerとして起動していたframeworkIDを指定して実行するという方法。

 

play run --http.port=$PORT --%worker

 

当たり前だけどこっちはちゃんと動きます。
これでWorkerとして定義したJobは実行できるようになったけど、Schedulerは永続的なプロセスではないのでJobが終了したらPlayをシャットダウンするように修正しないといけません。もうこの際Jobが終了したらそこでstopしてしまえば良いでしょう。

実際にテストで使用したクラスはこれ

 

import play.jobs.Job;
import play.jobs.OnApplicationStart;
import play.Play;
import java.util.Date;

@OnApplicationStart
public class HerokuTest extends Job {
	
	public void doJob() {
		if ("worker".equals(Play.id)) {
			System.out.println("framework id: " + Play.id);
			System.out.println("Job start: " + new Date() + ", " + Play.id);
			try {
				Thread.sleep(5 * 60 * 1000);
			} catch (InterruptedException e) {
				System.out.println("Job Interrupted: " + new Date() + ", " + Play.id);
			}
			System.out.println("Job end: " + new Date() + ", " + Play.id);
			new Thread() {
				public void run() {
					Play.stop();
					System.exit(0);
				}
			}.start();
		}
	}

}

 

stopするのに新たにスレッドを作っているのはOnApplicationのJobはPlay#start内で動くのでそこを抜けてから終了したいからです。(Play#startとstopはいずれもsynchronizedメソッドなのでstartを抜けるまでstopはブロックします。)

スケジューラのドキュメントによると数分を越えるような長い処理はWorkerを使えとありますが、たいていの場合はこれで十分でしょう。
ていうかWorkerが使えないからこんなことやってるんだっちゅーの(--

 

★まとめ

herokuのWorkerは良い仕組みだと思いますが、ちょっと惜しい感じです。
多くの場合Workerでは再起動のタイミングまでコントロールしたい気がします。(HerokuAPIを使ってアプリの中から制御するということもできそうですけど)

あとSchedulerでherokuコマンドは是非使えるようになってほしい!
いつだかのミートアップであった夜間はDynoの数を減らしたいという要件はこれができれば解決という気がします。

2012年9月14日 (金)

Heroku Meetup #6レポート その2

2日続けてこんにちは。Heroku Meetup #6レポの続きです。

今回はCodeConsultingにおける質疑とその後の懇親会で聞いた内容についてのレポートですが、これは小西が直接聞き取れた内容と通訳の方の翻訳を脳内補完した超訳です。
事前にある程度の回答予測をした上で質疑に臨んでいたこともあって、書きながらどこまでが実際にHeroku技術者の言葉でどこからが自分の推測なのかよくわからない部分があります。(^^;;;

なので、そんなこと言ってねーよ!と言う部分があれば是非ご指摘ください。m(_ _)m

 

★Scalabilityに関する質問

高負荷なアプリケーションを作成する際に注意する点としては何を考慮するべきだろう?ということを考えながら用意した質問です。

Q. DBのconfigを変更することはできないか?

A. できない

Q. DATABASE_URLは何に使われているか?また削除しても問題ないか?

A.
Rubyアプリではデフォルトで接続するDBの定義として参照する
Dyno数に応じてコネクションが増える件については詳しく調査していないが、削除しても問題はないはず。

Q. Routerの限界を突破する方法はあるか?

A. 何言ってましたっけ???
リクエストがキューにたまってそれが順番に処理されていくというRouterの基本的なアーキテクチャの説明とアプリがレスポンスを30秒間返してこない場合はRouterが503を返す(この場合はH12のエラーログが出ます)と言った説明がされていたことは覚えています。
ログがでないケースについても言及されていた気がしますが、ちょっと忘れてしまいました。

Q. 複数のDB接続をDynoごとに振り分ける方法はあるか?

例えばセッションIDでDynoを振り分ける(いわゆるSticky Session)や、EvernoteのようにURLの一部にShardingIDを含めるなどの方法が考えられると思うが何か良い方法はないか?

A.
Sticky Sessionはやらない。
Evernote的なShardingIDの可能性については。。。何か方法を検討してみるよ。的な回答だったと思うがいまいち自信がない。

Q. 高負荷のアプリがある場合Herroku内の他のアプリやネットワークにどのような影響があるか?

A.
CPUやメモリに関してはDyno毎に振り分けられているので影響はないはず。ネットワークについては何かしらの影響があるかもしれない。
しかしHeroku上では毎秒15000リクエストをさばくような高負荷のアプリも存在するが特に問題は起きていない。


全体的にはだいたい予想通りの回答です。
DevCenterでもかなりの情報が公開されていますが、アーキテクチャに関してもシンプルで特にトリッキーなことをしていないというのが個人的なHerokuの印象です。(なので、こうした場合どうなるかという挙動の予測がしやすい。)

Routerに関しては終わってから調べたことも少しあるので後で補足します。

 

★Securityに関する質問

Q. Heroku内部での通信の安全性はどのように担保されているのか?

負荷テストの過程ではアプリを分割することやPostgreSQLをやめてRDSやNoSQL系のDBを使うこともなんとなく考えていました。
ただその場合それぞれのコンポーネント間の通信は全部SSLにしないといけないのがコスト高そうだなぁとか考えてふとHeroku内部の通信はどうなっているんだということを考えたのがこの質問の背景です。

A.
Routerから各Dynosへの通信はhttp。Firewallの中での通信なので問題ない。
ログサーバーとの通信もFirewallの中。特に暗号化はしていない。

PostgreSQLはFirewallの外。機密性の高い情報であるならSSLを使うべき。
Rubyの場合はデフォルトでSSLを使用する設定になっている。

Addonはそれぞれだが考えたかとしては暗号化が必要。
Herokuアプリから他のHerokuアプリをコールする際のSSLの必要性についてはアプリ毎に判断してください。


うーん(--
PostgreSQLはHeroku謹製だからFirewallの中だと思ってたよ。。。
よくよく考えると同じURLでクライアントPCからも接続できているのでそんなはずはないんだけど。

で、あらためてDevCenterのドキュメントを見に行ってみると、やっぱりRemoteから接続する場合以外はSSL不要みたいな書き方になってんじゃん!!!

この際Herokuアプリから接続する際にもSSL必須でも良い気がするんだけどどうなんだろう?
パフォーマンスはまだしも言語によってはSSL用のドライバがなかったりするのかも。

しかし、これPostgreSQLの方ではどうやってHerokuアプリとそうでないものを見分けてるんだろう???(IPの範囲とか?)

 

★番外

他社からの質問でHireFireというDynoを自動で上げ下げするツールが紹介されました。
これはレスポンスタイムを計測してプロセスを管理しているということだったので、どこでレスポンスタイムを計っているんだろう?と聞いてみたところ。。。

「多分Gem。RouterからforwardしたHttpリクエストのヘッダにはRouterがリクエストをアクセプトした時間が書かれているから、それから計算しているんだと思う。」(超訳)

みたいな回答。え?!それじゃRubyでしか動かないじゃん!と思ってサイト見に行くと本当にRubyでしか動かないみたいね。。。。(^^;;;

こういうのがアリならPlayPluginをAddOnとして作成するのもアリかも。(嬉しい人がいるかどうかは謎だけど。。。)

 

★補足1 - ルーターのIPアドレス

今回行った負荷テストはほとんど「https://xxx.herokuapp.com/....」というアドレスに対して行いました。

本番はもちろん正規のSSL証明書をインストールした独自ドメインです。やったことがある方はご存知かと思いますが、SSL証明書をインストールすると「xxx.herokussl.com」という新たなルーターホストが作成されます。

で、これらのホストについてnslookupでIPアドレスを引くと。。。

  • xxx.herokuapp.com - 1個のアドレス
  • xxx.herokussl.com - 3個のアドレス

が返ってきます。

ドキュメントには書かれてませんが、SSL-Endpointアドオンではルーターも冗長化されてるってことですかね。

今回のテスト(herokuapp.com)では1万スレッドからの同時アクセスでルーターがパンクしましたが、herokussl.comの方を使用すればもっと多くのアクセスに耐えたのかもしれません。

 

★補足2 - StressToolのhttps

StressTool自体もPlayで作っており、そこからのHttpsリクエスト送信にはPlayのWSクラスを使用しています。

テスト中はあまり気にしてませんでしたが、この時のSSLハンドシェイクが何回行われているかをソースコードで追ったところ。。。。どうもKeep-Aliveが効いているっぽい。(多分)

つまり各スレッドで最初の1回だけハンドシェイクが行われて後は同じコネクションを利用してリクエストを投げ続けていたと思われます。

これだとテストが実運用時のリクエストを忠実に再現していないという意味でよろしくないです。(実際にはハンドシェイクがもっとたくさん行われてルーターの負荷があがると思われる。)

しかし今回の場合は性能指標がDBのInsert件数だったので、まぁ結果オーライです。(^^;;;

逆にハンドシェイクを抜かせばSSLはそんなに重くないという傍証になっているかもしれません。(この辺知識先行で経験則がないです。。。)

ちなみにHerokuのアクセスログに記録される「service=XXms」という時間はアプリでの消費時間のみを表しているらしいので、ハンドシェイクを含むルーターでの消費時間は多分含まれていません。

2012年9月13日 (木)

Heroku Meetup #6レポート その1

こんにちは。Heroku Meetup #6に参加してきました。

今回私はCode Consultingのプレゼンターの一人として発表させてもらいましたが(自分にとって)非常に有益な時間だったと感じています。
貴重な機会を与えてくださったHeroku社の方々に感謝します。

さて、そんな私のプレゼンですが後で懇親会で何人かの方から感想を伺ったところによると途中からよくわからなくなったという人多数。(--

あいすいません。m(_ _)m
昔から聴き手を置き去りにするプレゼンには定評があるんですが、今回は自分でも詰め込みすぎだったと思います。
また時間と同時通訳を考慮してカットした話も多いです。

なので、ここで完全版(?)の説明をお届けしたいと思います。当日使った資料は一応ここにありますが別に見なくても良いです。(^^;;;

 

★単純明快なシステム概要

まずネタにしたアプリですが、HTMLの装飾とかをとっぱらってシステムの骨子だけをとりだせばWEBフォームに入力するデータをDBにINSERTするだけという超単純なモノです。
例によってPlayframeworkで作りましたがサーバー側のコード自体は誰が作ってもこうなるだろう、というものでしたし別の言語を使っても似たり寄ったりだったでしょう。

はっきり言ってこれだけだとまったく箸にも棒にもかからないシロモノなわけですが、これがちょっと他と違っていたのは、そのサイトが毎年夏にやっている超有名テレビ番組から宣伝されるということです。

なので、URLがテレビに流れた直後に嘘みたいなアクセスが集中します。

 

★冗談みたいな性能要件

さて、このシステムをHerokuで実装するという前提で話を聞いたわけですが、割と最初の段階からボトルネックとなるのはDBのトランザクションだろうという予測がありました。

なので、「1秒に何件くらいINSERTできれば良いんですかね?」と聞いたところ最初の段階では900件という回答でした。
多めに見積もって1000件/秒と考えましたが、この時点ではそれ位はいくんじゃないかと思ってました。(以前に単純なテストでRonin * 2Dynosで200件/秒の登録ができることを確認していました)

。。。が、この数字が何故か数日後には5000件/秒になります。。。(--

どうもこの数字がユーザー数やPVと混同されたらしく、仮にPVがそれだけあってもトランザクション数はそこまでにはならない、ということをいくら説明してもとりあってもらえなかったようなんですね。

あとで別のところから聞いた話によるとテレビでURLが流れた場合、かなり高い確率で503 Service unavailabe(要するに過負荷でサーバーに接続できない)が返ってきてがっかり!となるため性能要件には神経質になっているということもあるようです。

なので結局僕のところでは秒5000でやってくれという話でおりてきました。。。(--
ちなみにこの時点で放送日まで2週間きってます。

最初から5000って言われてたらRDBは使わねーよ(--

 

★PostgreSQLを落とす

そんなこんなで負荷テストを開始します。
幸いなことに今回クライアントとHerokuとの契約はミートアップでも紹介されていたパッケージのさらに上位グレードの契約だったのでDynoもAddonもほぼ使い放題です。

そこでまずはPostgreSQLの限界を見極めることからテストを開始することにしました。
DBはそれほどキャッシュが必要なシステムとも思わなかったのでなんとなくIkaを選択します。

Heroku上ではPostgreSQLのconfigがMAX_CONNECTIONS=500で設定されていることはあらかじめ調べて知っていたのでとりあえずはこれを突破した時にどのような挙動になるかを確認することしにしました。
予算には余裕があるのでDynoは100台あげます。(^^;;;
Playframeworkの設定でスレッド数は50。コネクションプールは30です。

テストツールとしてはJMeterを使用して1000スレッドで100回ループさせました。(合計10万件のINSERT)

テスト条件 DB Dyno ServerThread ConnectionPool Client ClientThread LoopCount
Ika 100 50 30 JMeter 1000 100
結果 ConnectionError多数

予想通りの結果ですがDriverManager#getConnectionでエラーとなります。

HerokuはWebシステムとしてはDynoを増やすことで自在にScaleしますが、DBはScaleしないので単純にDynoだけ増やしても意味がないってことですね。

PostgreSQLのconfigは一切変更不可能なので500しかないコネクションをいかに有効に使うかが勝負になります。

そこで次にDynoの数を15まで減らしてみます。Playのスレッド数とコネクションプールは変更していないので最大コネクション数は「15 * 30 = 450」となるはずです。

テスト条件 DB Dyno ServerThread ConnectionPool Client ClientThread LoopCount
Ika 15 50 30 JMeter 1000 100
結果 250件/秒くらい

思ったほど数字伸びてないですね。(--;;;

 

★Heroku上にStressToolを実装

この時点では負荷テストのクライアントツールにローカルPC上のJMeterを使用していましたが、NewRelicで見る限りサーバー側の負荷がそれほどあがっているようには見えませんでした。にも関わらず設定をどのように変えてもあまり数字は伸びてません。

これには二つの理由が考えられます。

ネットワークレイテンシの問題

Herokuはアメリカ東海岸にあるので通信には毎回太平洋越えのネットワークレイテンシが発生します。レイテンシが平均100ms前後だとするとクライアント側で多数のスレッドがぐるぐるとループしていても各スレッドは最高でも100msに1回しかリクエストを発行していない計算になります。

クライアントリソースの問題

JMeterで1000スレッドを指定したとしてもクライアントPCの性能がそれに追いついていなければ実際に1000スレッド同時には動けません。
実際JMeterのログを検証しても早いスレッドはテストが半分完了する以前に終了していますし、最後の方は最初の競争に負けたスレッドがまとめて動いているような感じでした。
また、1000スレッド以上の負荷、例えば5000スレッド同時に動かそうとしてもクライアント側のソケット不足でまともに動かないという問題もあります。

要するにクライアント側の問題で実はHerokuにはたいして負荷がかかっていないんじゃないかというのがこの時点での推測です。

なので、ネットワークレイテンシのない東海岸からテストを実行するためにHeroku上にStressToolを作成することにしました。
ツール自体はURLをキックしたら指定個数のスレッドを起こして指定回数ループするだけのごく単純なモノです。

これをDyno100台あげて、100回キックすればHerokuルーターのラウンドロビンによっていい感じにクライアントの負荷が分散されるはずです。(^^;;;

テスト条件 DB Dyno ServerThread ConnectionPool Client ClientThread LoopCount
Ika 14 50 30 Heroku 5000 100
結果 1000件/秒くらい

とりあえず当初の目標は達成しましたね。(^^v

 

★Herokuのルーターを落とす

凶悪なStressToolを手に入れたので今度はHerokuのルーターがどこまでの負荷に耐えられるのか実験してみます。クライアントスレッド数を一気に倍の10000スレッドにしてみました。

テスト条件 DB Dyno ServerThread ConnectionPool Client ClientThread LoopCount
Ika 14 50 30 Heroku 10000 100
結果 503と接続エラー多数

ルーターのキューがあふれた場合はHerokuはログも吐かずに503を返すようです。
さらに負荷がかかった場合は多分接続エラーになっていると思います。(この辺はStressToolの作りが雑だったため正確には検証できてません。。。)

Webサーバーの動作としては割と一般的だと思いますが、503を返す場合はサマリだけでもログを出力してほしいですね。

ちなみにあとでログを調べたところ5000スレッドでもログサーバーは時々限界を越えているらしくところどころで「L10 - Drain buffer overflow」が記録されています。
最悪入力データをJSON化してログに書くだけにしようかとも思ったんですが、その案も没です。(そうは言っても一番多いところで秒間26000行以上のログが出力されてます。(^^;;;)

あまりに高負荷をかけるとHeroku社に怒られそうな気がするので1万スレッドでのテストは1度しかやっていません。(^^;;

 

★3DBでシャーディング

その後いろいろと設定を変えてテストしてみましたが、どうやっても数字はだいたい1000前後で止まります。このことから、これはもうDBがいっぱいいっぱいなんだろうと思いました。だけどDBのconfigは変えられないんですよねぇ。。。(--

そこで1DBをあきらめて複数のDBを使用して負荷分散させることを考えました。具体的には入力データの必須項目であるメールアドレスからハッシュをとってその値で使用するDBを切り替えます。
何故メアドのハッシュを使用するかというと要件の中にメアドの重複チェックだけは入っていたからです。

コネクションプールはPlayの標準のものが使えないので自前で実装。サイクリックにコネクションを使いまわしてオープンからある程度時間のたったものは破棄する単純な実装です。

DBは本当は同じグレードのものを使用するのが良いんですが、わざわざそのためだけにアプリを作るのも嫌だったので同一アプリ内でRoninとFuguを追加しました。

テスト条件 DB Dyno ServerThread ConnectionPool Client ClientThread LoopCount
Ika, Fugu, Ronin 14 50 30 * 3 Heroku 5000 100
結果 Out of memory
パラメータ調整しても900件/秒くらい

まったくもっていまいちです。。。(--

まず、最初に1DBの時と同じ設定ということで50スレッドと30のコネクションプールで動作させましたが、これはOutOfMemoryとなるDynoがいくつかでました。
実際にいくつのコネクションプールが作られたかはわかりませんが、最大で90個のコネクションがメモリに保持される計算になるので。
Herokuの各Dynoに割り当てられたメモリは512MB。けっして多くはありません。

そこでスレッド数やコネクションプール数を調整しながら何回か実行しましたが、結果は一番良い時で926件/秒。スループットは上がるどころか落ちてます。(--

正直この結果は予想外でしたが、結果から中で起こっていることを推測すると以下のようになります。

  1. 各Dynoは1DBの時と同じくらいの仕事量をこなしている
  2. 各DBの仕事量は分散によって3分の1に減っている
  3. 各DBの接続数(各Dynoでのコネクションプール数)は1DBの時と同じか少ないはず
  4. DBの同時トランザクション数は約3分の1のはず
  5. にも関わらず各トランザクションのレスポンスタイムはあまり変わっていない???
  6. スループット低下の原因はRonin, FuguがIkaよりもスペックが低いためと推測

いまいち5番目のレスポンスタイムの項が納得いかないですが、手持ちのデータから結果を無理やり説明しようとするとこうなります。

(他に何か説明可能な推測がありますかね?ちなみに自前のコネクションプールは1DBで使用した場合にPlay標準のコネクションプールを使用した場合よりも若干パフォーマンスが良いくらいなので多分問題ないです。)

エンジニアとして原因を追究したいという気持ちはあるんですが、これを検証しようとするのは結構難儀な作業です。
なんせ負荷テストをやっていた期間のログはほとんど5GB超。Papertrailでログのアーカイブはとっていましたが、翌日にならないとログを確認できないしそれをまともに開けるエディタもないという状況だったので。。。時間的にちと厳しい(--

まぁそれは置いておくとしても先のテストと併せて考えるとIkaの処理能力を使いきってないのでアプリの改修でスループットの向上が見込めることは間違いありません。
ただ、それも各DynoがすべてのDBに接続する可能性があるという条件の下では実装が難しくなる気がしました。
Herokuルーターが同一セッションのリクエストをどのDynoに飛ばすかは制御できないので、1Dynoが1DBにのみ接続するようなアーキテクチャもプラットフォーム側になんらかの手を入れない限り難しそうです。(今回のケースではDynoのIPアドレスからハッシュをとって接続DBを切り替えて後から重複チェックをするというのもギリギリありではあったんですが。)

先のOutOfMemoryの件もあるのでこの路線にはあまり未来が感じられずこの段階で一旦没としました。

 

★DATABASE_URLの削除

少し話はそれますが、この頃までにどうもConnectionErrorの発生する閾値が自分の予想よりも低いことに気が付いていました。
設定上で420コネクションしか使わないように調整してもConnectionErrorが発生することがあるのです。
PostgreSQL自体がいくらかのコネクションを予約していて500個全部が使えないのはわかるのですがなんか妙に少ない。

そこでDyno起動直後のセッション数をpg_stat_activity表から数えてみると、明らかに自アプリからの接続ではないコネクションが多数存在します。
それもDyno数に比例して増減しているようでした。

つまりパフォーマンスを求めてDyno数を増やせば増やすほど利用可能なコネクション数が減っていくという悪循環があったわけです。

これは要するにHeroku自体が裏でDBに接続して何かやっているということなんでしょうが、何をやっているのかは謎です。

試しに環境変数からDATABASE_URLを削除してみたところ謎のコネクションはなくなり、特にアプリの動作にも影響がなかったので結局削除したままで本番まで運用しました。

これで利用可能なコネクションが増えてConnectionErrorはでなくなりましたが、スループットの方にはそれほど極端な差は見られませんでした。

 

★Mecha出陣!

さて、もういい加減手詰まりな感があったんですがここで頼れるCTOから貴重なアドバイスが。

 

「EC2のラインアップを見ればPostgreSQLの各Planの土台となっているインスタンスはほぼ推測できる。
PlanによってCPUスペックがまったく違うからそれ相応のパフォーマンスアップが期待できるはず」

 

言われてEC2のインスタンスタイプのページを見に行くと確かにどこかで見たような数字が並んでいます。

これを見るとIkaの土台はおそらくラージインスタンス(4ECU)。
対して最上級プランのMechaの土台はハイメモリクアドラプルエクストララージインスタンス(26ECU)。
なんじゃ、そら。

Mechaは1ヵ月使うとさすがに予算オーバーなんですが、今回期間が短いので日割りにするとぎりぎりパッケージ予算内に収まります。
試しに実行してみた結果がこれ。

テスト条件 DB Dyno ServerThread ConnectionPool Client ClientThread LoopCount
Mecha 15 50 30 Heroku 5000 100
結果 4189件/秒

なんといきなりの4000件超!!!
なんじゃ、そら。

結局何度かテストを実施したところ一番良い時で4813件/秒まで行きました。 この数字はテスト全体でのスループットなので一番負荷の高いところから1秒を抜き出せば5000件/秒をクリアしています。

長々と書いてきて最終的な結論がパフォーマンスは金で買えということかと思うとかなりがっかりですが、まぁとりあえずまさかのミッションコンプリートです。

それにしてもCPUスペックは重要ですね。Heroku PostgresのページにはPlanの違いはCache sizeの違いとしか説明されてませんがCPUスペックも併記してほしいものです。

 

★放送当日

テレビの影響力を体感しましたがそれでも楽勝でした。(^^;;;

 

★質疑応答

このあとようやくメインイベントの質疑応答ですが、長くなったのでまた今度(^^;;;

2012年9月 7日 (金)

JavaでHerokuのWorkerを作成する

こんにちは。

このブログで何度も取り上げていることからもわかると思いますが、FLECTでHerokuアプリを作成する場合の第一選択は Playframework 1.2.xです。

これは当社にJavaの技術者が多いことが最大の理由ですが、Playの生産性は非常に高くパフォーマンスも良いので Webアプリを作る分には何の問題もありません。

その一方でHerokuのWorkerをJavaで作る場合はどうするのが良いんだろう?ということがずっと自分の中で課題として残っていました。
自前でmainを持つサーバーアプリケーションを作ってしまえば良いんですけど毎回それはさすがに面倒ですもんね。

そんな折ちょっとした示唆があってWorkerもWebアプリと一緒にPlayで作っちゃえば良いじゃん!ということに気がつきました。

以下はその方法です。

 

★PlayでJobを作成する

Playframeworkを使ったことのある人ならご存知かとは思いますが、PlayにはJobという強力なスケジューリング機構があります。

  • 何秒おき、何時間おきといった指定間隔でのインターバル実行
  • 毎日何時に実行

といったスケジューリングがAnnotationの宣言だけでできるのです。

この機能を使えばWebDynoを作る際にバックエンドJobも同時に組み込むことができますが、実のところそれはあんまりうれしくありません。

何故ならWebDynoは負荷によってスケールさせたいので何台起動するかがわからないからです。WebDynoが5台あがっていたらバックエンドJobも5回動作することになりますが、多くの場合これは意図するところではないでしょう。

なので定義したJobはWebDynoでは動かしたくありません。あくまでWorkerとしてだけ動かしたいわけです。

実はこれ簡単にできます。
以下にサンプルを示します。

 

import play.Play;
import play.jobs.Job;
import play.jobs.Every;
import java.util.Date;

//@Every("10s")
@Every("${jobtest.every}")
public class JobTest extends Job {
	
	public void doJob() {
		System.out.println("Job: " + new Date() + ", " + Play.id);
	}

}

 

上記は日付とPlayのフレームワークIDをコンソールに出力するだけの単純なJobです。
ポイントは@Everyの宣言でパラメータを使用していることです。

@Everyまたは@Onの引数に「${xxx}」というパラメータを使用した場合、その値はapplication.confから読み込まれます。

 

★application.confでスケジュールを定義

あとはフレームワークIDごとのスケジュールをapplication.confに追加してやればOKです。

デフォルトの設定で使っている場合は、

  • ローカル開発環境ではフレームワークIDなし
  • 本番(Heroku)環境では「%prod」

としている人が多いと思いますが、その場合の設定は以下のようになります。

 

jobtest.every=10s
%prod.jobtest.every=never
%worker.jobtest.every=5s
%worker.application.mode=prod

 

「%prod」「%worker」はそれぞれWebDyno用、WorkerDyno用のフレームワークIDです。
application.confでは同じキーの設定が複数回出現した場合は後に記述されたものが有効になるので、それぞれの環境毎の設定が行えます。
設定値を「never」とした場合そのJobは実行されなくなります。

最後の「%worker.application.mode=prod」はWorkerDynoでのPlayをProductionモードで動作させるための設定です。
DEVモードでは最初のHTTPリクエストが届くまでPlayは動作しないので、これがないとWorkerDynoはうまく動きません。

 

★Procfileの作成

最後にWorkerでもPlayを動かすようにProcfileを定義します。

 

web: play run --http.port=$PORT $PLAY_OPTS
worker: play run --http.port=$PORT --%worker

 

上の例ではWebDynoのフレームワークIDはHeroku環境変数「PLAY_OPTS」で、WorkerDynoのフレームワークIDは直接指定していますがこの辺はお好きなように。

WorkerDynoでもportにbindしてHTTPリクエストを待ちうけていますが、HerokuのルーターはWebDynoにしかリクエストをforwardしないのでここにHTTPリクエストが来ることはありません。

 

★メリット、デメリットを考える

いかがでしょう?(^^;

この方式ではWebDynoとWorkerDynoを一つのアプリケーション内で同時に作成できるのが最大のメリットです。
ローカル環境で開発/テスト中はWebとWorkerを1プロセスの中で同時に動かすことができます。

WebDynoでは使用しないJobのインスタンスがひとつ余計に作られることになりますが、その程度ではアプリの動作に何の影響もありません。

WorkerDynoはまったく使用しないWebアプリをまるまる抱えることになり、それはまぁちょっと気になるところではありますが。。。。
おそらくWorkerDynoのスペックもWebDynoと同等と思われるので気にすることはないでしょう。

WorkerDynoが待ちうけているportは多分外からアクセスできないと思うのでこれも問題ないはず。
ここにリクエストが飛んでくるようならそれはHerokuのセキュリティホールです。(^^;

Heroku上にステージング環境を持つ場合でもフレームワークIDでどうにでも対応できるので柔軟性にも問題なし。

他に何か考慮すべきことあるかな???

ん~、圧倒的にメリットの方が多い気がします。
こうして考えるとPlayでHerokuアプリを作る場合Workerはこれがスタンダードでも良い気がしますね。(^^v

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回分の引数オブジェクトがメモリに残ります。

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

2012年7月24日 (火)

PlayとMySQLとサロゲートペア

MySQLで文字コードを「utf8」とした場合UnicodeのBMPしか扱えないそうです。

http://yanok.net/2010/06/mysqlutf-8.html

サロゲートペアを扱う必要がある場合は最初から文字コードを「utf8mb4」として定義しないといけないらしい。。。(--
なんとも残念な仕様です。「Unicode対応」と言いつつサロゲートペア未対応のものはたまにあるので、そこにはそれほど驚きませんが対応するのであれば「utf8mb4」みたいな新しいキーワードを導入するのではなく「utf8」として対応してほしかったと思います。

DB作成の際には文字コードは意識しますが何の迷いもなく「utf8」を選ぶっちゅーの(--

そんな愚痴はさておいて、ここではPlayframework(1.2.5)からMySQLのutf8mb4なデータベースを扱う話をします。

MySQLでutf8mb4のデータベースを作成する方法については色々なところで紹介されているのでここでは特にとりあげません。それだけはまった人が多いってことですよね(^^;;;

さて、MySQLのutf8mb4化も終わり次にPlayのアプリケーションからINSERT/SELECTのテストを試みたんですが、最初はやっぱりサロゲートペアが正しく扱われませんでした。

データベース側の設定がまだ足りてないのかといろいろ試行錯誤したんですが、これ実はPlay側の設定の問題でした。

PlayではDBの設定はapplication.confで行うのですが、その設定方法には

db=mysql://user:pwd@host/database

のように1行にユーザー名やパスワードなどのすべての情報を記述する方法と

db.url=jdbc:mysql://host/database
db.driver=com.mysql.jdbc.Driver
db.user=user
db.pass=password

のように4行にばらして指定する方法があります。
1行でまとめて指定する方法が使用できるのはPostgreSQLやMySQLなどのPlayが標準で対応しているDBだけでOracleやSQLServerを使用する場合は常にばらした記述方法を使用しなければならないのですが、可能な場合は1行設定を使用することの方が多いかと思います。

ここに罠があります。
1行設定は内部的に4行の個別設定にばらされるのですが、この時にdb.urlが

db.url=jdbc:mysql://host/database?useUnicode=yes&characterEncoding=UTF-8&connectionCollation=utf8_general_ci

のようにエンコーディングのオプションをハードコードで付加された形で設定されてしまうのです。
このためutf8mb4のデータベースを使用する際は1行設定は使用することができず

db.url=jdbc:mysql://host/database?useUnicode=yes&characterEncoding=UTF-8&connectionCollation=utf8mb4_general_ci
db.driver=com.mysql.jdbc.Driver
db.user=user
db.pass=password

のようにutf8mb4を明示してバラに設定する必要があります。

2012年7月20日 (金)

Herokuの負荷テスト

こんにちは。小西です。
Herokuの負荷テストの結果を公開したので興味のある方はご覧ください。

http://excelnote.herokuapp.com/share/note/s91/90bf7b19-d654-40ab-a6c0-640980967ab0/edf9ab2230042e456ca286843763788e

考察は自分用のメモなので文章が荒いのは気にしないように。(Evernoteだし)

えぇ、このエントリの主目的はきっぱりはっきりDEVCUPの宣伝です。(^^;
ちょっとでも役に立ったという方は是非ExcelNoteに一票投じてください。m(_ _)m

ExcelNoteにはシートの一部のみをiframeで表示する機能もあるので、表の部分だけをこちらのブログに貼り付けようかとも思ったんですが、表が少し幅があるためブログにきれいに収まらないことからこういう形をとりました。

背景の白い部分がExcelで作成した資料なわけですがコメントがつけられたりリンクが貼れたりするのは地味に便利です。
ちなみにExcelNoteのサンプル一覧はこちら

https://www.evernote.com/shard/s91/sh/ff42fdc3-a143-4019-a1f6-9828fb03faa9/3f441690243667851b8711e2e82939a3

一般投票期間中はできるだけサンプルを増やそうかと思ってるですが、普段Excelをほとんど使わないのでネタがなくて困ってます。(--

さて、これだけでは何なので負荷テストの結果を踏まえてHerokuとPlay(1系)でアプリを作成する際のポイントを簡単にまとめておきます。

・Memcachedは必須
dynoを増やすとリクエストはラウンドロビンで各dynoに割り振られます。
セッションは考慮されないのでセッション変数的なものを使用する場合はMemcacheが必須になります。
(Playのsessionの実体はCookieなので文字列しか保存できないしセキュリティ的に使えないことも多い)
ちなみにHerokuのアドオンとしてはMemcacheとMemcachierの2つがありますが、無料枠が大きいのでMemcachierの方がお得です。

・パフォーマンスは金で買える
単純なラウンドロビンなので基本的にはDynoを増やせばそれだけで負荷分散できます。
もちろんアプリの中でstaticな変数を使ってはいけないなどスケールさせるために考慮しなければならない事項はありますが、それ以外はアクセス数やメモリ使用量に関してそれ程シビアに考える必要はなさそうです。

・負荷はNewRelicで計測
Web transactions -> Slowest average response time
Dynos -> Average memory usage per dyno

あたりを眺めていれば、なんとなくDynoの増減のタイミングはつかめそうです。

・ログはPaperTrailがお勧め
Herokuのログ管理アドオンはやたらとたくさんあって、いくつか試してみたんですが個人的にはPaperTrailがダントツでお勧めです。
WebUIも見やすいですし、APIでログを自由に取得できるのも良いです。
Herokuのアクセスログには処理時間やDyno番号など結構な情報量があるので、いずれはこのログを解析して負荷や傾向を分析するツールも作成しようと思っています。
あと地味に日時表示のタイムゾーンを変換してくれるのもありがたいです。

・application.confに「XForwardedSupport=all」を必ず設定する
Herokuへのアクセスがhttpsであっても、そこから各dynoへのリクエストはhttpで行われます。
そのため、この設定がない場合リダイレクト時に元がhttpsであってもhttpにリダイレクトされてしまいます。
これは知らないと気がつきにくいので注意が必要です。

とりあえずはこんなところですかね。
他にもいろいろあるんですが、それはまたおいおい。(^^;

2012年7月 2日 (月)

Play1とPlay2の比較 - まとめ編

ども。小西です。
ちょっと間が空いてしまいました。

その間に世の中ではplay2.0.2がリリースされました。

https://groups.google.com/forum/#!msg/play-framework/Z97GQ2VnR5M/T-STGaeuN68J

リリースメールを見る限り大きな変更点はなく、バグ修正といけてないコードのリファクタリングが主なんではないかと思います。
手元のアプリをバージョンアップしてみても特に差は感じませんでした。

さて、数回にわたって書いてきたPlay1とPlay2の比較記事もそろそろたたんでしまいたいと思います。
今回は使いながら気がついた細かい点をつらつらと書いてみます。




★heroku編
Play2は普通にherokuで動作しますが、新規アプリケーションを作成して1行もコードを書かない状態でpushしてもいきなりSlugサイズが40MBを超えます。
Apache POIなど大き目のjarファイルをいくつかインクルードするとそれだけで60MB超。困ったことにpushにかかる時間は約2分です。

herokuのドキュメントには「Slugサイズが50MBを超えたら構成を見直せ」みたいなことが書かれているんですが、フレームワークだけでそのサイズを超過するのでもはやユーザーにできることはありません。。。(--

ちなみにPlay1でもイニシャルで20数MBになるので、こちらも重いフレームワークではあるんですけどね。

いずれにせよherokuで使うことを考えたらPlay2はちょっとお勧めできません。

 

 

 

★WS編
前にも書いたと思いますがPlay2のWebServiceクライアントであるWSクラスでは同期APIが削除され非同期APIのみとなっています。
非同期APIの方がスレッドを効率的に使えるという理屈はわかりますし、今後は非同期APIが主流になっていくというのもそうなのかもしれません。
でも

http://www.playframework.org/documentation/2.0.2/ScalaWS

のサンプルを見ると結局同期でレスポンスを返しているので意味ないんでは。。。と思ってしまいます。(--

実際JDBCをはじめGoogleやAWSのSDKもほとんど同期APIなので、ここだけ非同期にすることにどの程度の効果があるのかは疑問です。
結局のところWebサーバーの仕事はクライアントに対して何らかのレスポンスを同期で返すことなんで、複数の外部サービスにすべて非同期でアクセスして待ち合わせするようなケース以外では非同期のメリットは享受できない気がします。

であれば、WSから同期APIを削除したメリットはほとんどなくて先のサンプルのようにユーザーに非同期プログラミングを意識させなければいけないというデメリットだけが残っているように見えます。

個人的にはWSの同期APIは復活してほしいです。

 

 

 

 

★JSON編
JSONを扱うためのライブラリがGoogleのgsonからjacksonに替わっています。
ざっとドキュメントを読んだ感じだと、使い勝手はそれほど変わらない様だしパフォーマンスに関してはjacksonの方が大分速いようです。

これだけであればjacksonへの変更はむしろ歓迎なわけですが、フレクトでは社内のライブラリでgsonをメチャメチャ多用しているのでPlay2にあげた場合でもgsonを使い続けるという選択になります。(--

パフォーマンスという観点ではPlay1.2.4ではgsonのバージョンが1.7.1だったのが、Play1.2.5では2.2にあがるようなのでそこはちょっと期待しています。(2012/07/02時点の最新版は2.2.1です)

 

 

 

 

★その他のライブラリ編
これを書くに当たってBuild.scalaを見直していて思い出しましたが、log4jも標準では入っていません。
デフォルトのLoggerはslf4j+logback。
log4jの開発は事実上終了しているので新たに起こしたプロジェクトとしてはこの選択は妥当だと思います。

そうはいっても、なんかライブラリ入れるとすぐにlog4jも一緒に入っちゃいますけどね。

 

 

 

 

★まとめ
大体持ちネタは以上です。
今回Play2のソースはまぁまぁ読みましたが、実際にはそれ程多くのものを作ってはいないので、色々と勘違い等もあるかもしれませんがこうした技術系のブログを読む場合はそのすべてを鵜呑みにしてはいけないというのは基本だと思うので、そのつもりで軽く読み流していただければ幸いです。(^^;;;

読んでてわかると思いますが、現状では僕のPlay2の評価はそれほど高くはありません。良いところもたくさんあるんですが、それらはほとんどPlay2の長所というよりもScalaの長所なんですよね。
Scalaは好きな言語なんでそれを使って開発したいという気持ちはあるんですが、当面はPlay1を使うことになりそうです。

採用情報

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

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

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

フレクト採用ページへ

会社紹介

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