速いよ Java Play Framework
言語とFrameworkの選定
phpにはあまり魅力を感じていない@yutakikuchi_です。本題とは関係ありませんが4.25(金)@ヒカリエのイベントに登壇します。ネタは同窓会GrowthHackとログ集計/解析の2本立てです。興味のある方はどうぞ。【ヒカ☆ラボ】同窓会GrowthHack!×データログ集計、解析!をテーマに事例をまじえお話します! 16年ぶりの再会でも参加率6割の同窓会を開くには?Yahoo出身のエンジニアが語る、アクセスログ可視化、 ユーザ属性解析を行うためのシステム設計のコツとは?
Round 8 results - TechEmpower Framework Benchmarks
さて、本題に入ります。僕がphpを書き始めたのも前職のmain言語として指定されていたことがあり、あまり書いていて楽しく無いとは思っていながらも泣く泣く仕事としてやってい感じです。過去に何回かphpのエントリー書いてますけどそれも仕事で利用する為です。前職は本当にphperの集まりでWebだけじゃなくてバッチ処理やスクリプト処理も全てphpで書こうとする姿勢を初めて見たときは驚愕しました。まぁ速くコードを書くならそれでもいいんでしょうけど。
言語とFrameworkの選定にはドキュメント量、言語のCommitterや精通者がいる、必要なライブラリや機能がある、書き易い、チームの多数決等が基準となり決定されるケースが多いと感じます。僕なら「処理速度が速い」を正義とし、それに掛け合わせる形で「書き易さ」で選ぶと思います。時間コストを考える場合、もし新しい言語とFrameworkの導入で2人月掛かったとしても(2人月掛かることが許される場合)、Daily100万PVのサイトで0.2secリクエストが速くなったとしたら1日の処理削減コストは100万*0.2/(3600*24)の2.3人日。ということは一か月で導入コストの2人月は巻き返せる事になります。更にはユーザーがアプリを使った時の満足感も上がるはずですし。
僕は今Frameworkを使わずにCを書いていてphpの3倍以上の速さが出ているので満足しているんですが、書き易さの点からは本当に最悪な状態。メモリの動的確保/解放や配列処理を本当に間違える...まぁそんな事で処理が速いとされ、C言語より書き易いJava,ScalaやGoのFrameworkを少しずつ勉強かつ紹介していけたらなと思い、今日はPlay Frameworkについて書きます。Play Frameworkはソース更新後の最初のアクセスで自動的にJavaをrebuildしてくれるようなのでコンパイルの手間が省けてとても便利です。
Play Framework
環境の確認と設定
CentOSは6.4、Javaはjava-1.7.0-openjdk、playは2.2.2を使っています。
$ cat /etc/system-release CentOS release 6.4 (Final) $ yum list installed | grep java java-1.5.0-gcj.x86_64 1.5.0.0-29.1.el6 java-1.7.0-openjdk.x86_64 java-1.7.0-openjdk-devel.x86_64 java_cup.x86_64 1:0.10k-5.el6 @base tzdata-java.noarch 2014a-1.el6 @updates $ java -version java version "1.7.0_51" OpenJDK Runtime Environment (rhel-2.4.4.1.el6_5-x86_64 u51-b02) OpenJDK 64-Bit Server VM (build 24.45-b08, mixed mode) $ alternatives --display java /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java - 優先項目 170051 スレーブ keytool: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/keytool スレーブ orbd: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/orbd スレーブ pack200: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/pack200 スレーブ rmid: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/rmid スレーブ rmiregistry: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/rmiregistry スレーブ servertool: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/servertool スレーブ tnameserv: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/tnameserv スレーブ unpack200: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/unpack200 スレーブ jre_exports: /usr/lib/jvm-exports/jre-1.7.0-openjdk.x86_64 スレーブ jre: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64 スレーブ java.1.gz: /usr/share/man/man1/java-java-1.7.0-openjdk.1.gz スレーブ keytool.1.gz: /usr/share/man/man1/keytool-java-1.7.0-openjdk.1.gz スレーブ orbd.1.gz: /usr/share/man/man1/orbd-java-1.7.0-openjdk.1.gz スレーブ pack200.1.gz: /usr/share/man/man1/pack200-java-1.7.0-openjdk.1.gz スレーブ rmid.1.gz: /usr/share/man/man1/rmid-java-1.7.0-openjdk.1.gz スレーブ rmiregistry.1.gz: /usr/share/man/man1/rmiregistry-java-1.7.0-openjdk.1.gz スレーブ servertool.1.gz: /usr/share/man/man1/servertool-java-1.7.0-openjdk.1.gz スレーブ tnameserv.1.gz: /usr/share/man/man1/tnameserv-java-1.7.0-openjdk.1.gz スレーブ unpack200.1.gz: /usr/share/man/man1/unpack200-java-1.7.0-openjdk.1.gz 現在の「最適」バージョンは /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java です。 $ vi ~/.zshrc #2行追記 JAVA_HOME=/usr/lib/jvm/java-1.7.0/ export JAVA_HOME PATH=${JAVA_HOME}/bin:$PATH:/home/yuta/work/src/play/play-2.2.2/:/home/yuta/work/src/sbt/sbt/bin/ export CLASSPATH=$JAVA_HOME/jre/lib/ext:$JAVA_HOME/lib/tools.jar $ type javac javac is /usr/bin/javacinstallと実行
$ wget "http://downloads.typesafe.com/play/2.2.2/play-2.2.2.zip" $ unzip play-2.2.2.zip $ cd play-2.2.2 # helloworldプロジェクトを作成 $./play new helloworld _ _ __ | | __ _ _ _ | '_ \| |/ _' | || | | __/|_|\____|\__ / |_| |__/ play 2.2.2 built with Scala 2.10.3 (running Java 1.7.0_51), http://www.playframework.com The new application will be created in /home/yuta/work/src/play/play-2.2.2/helloworld What is the application name? [helloworld] > helloworld Which template do you want to use for this new application? 1 - Create a simple Scala application 2 - Create a simple Java application # ここではJavaを選択 > 2 OK, application helloworld is created. Have fun! $ cd helloworld $ ../play run ../play run Getting org.scala-sbt sbt 0.13.0 ... :: retrieving :: org.scala-sbt#boot-app confs: [default] 43 artifacts copied, 0 already retrieved (12440kB/579ms) [info] Loading project definition from /home/yuta/work/src/play/play-2.2.2/helloworld/project [info] Set current project to helloworld (in build file:/home/yuta/work/src/play/play-2.2.2/helloworld/) [info] Updating {file:/home/yuta/work/src/play/play-2.2.2/helloworld/}helloworld... [info] Resolving org.fusesource.jansi#jansi;1.4 ... [info] Done updating. --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Ctrl+D to stop and go back to the console...) [info] Compiling 4 Scala sources and 2 Java sources to /home/yuta/work/src/play/play-2.2.2/helloworld/target/scala-2.10/classes... [info] Error occurred during initialization of VM [info] Could not reserve enough space for object heap [error] Error: Could not create the Java Virtual Machine. [error] Error: A fatal exception has occurred. Program will exit.初回実行時はJavaのheapエラーで怒られてしまいました。play-2.2.2/framework/buildファイルのheap領域指定のオプションを修正します。僕は自分の環境に合せて-Xmx1536Mから-Xmxを768Mに修正しました。ぐぐると同じディレクトリにあるbuild.batを修正する内容も見かけるので、そちらの-Xmxも合せて修正しておくと良いと思います。修正後にplay runで問題なく起動します。9000ポートで見れるように解放も忘れないようにしましょう。
$ cd framework $ vi build # Xmxを768Mに修正 "$JAVA" ${DEBUG_PARAM} -Xms512M -Xmx768M -Xss1M -XX:ReservedCodeCacheSize=192m -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=512M ${JAVA_OPTS} -Dfile.encoding=UTF-8 -Dplay.version="${PLAY_VERSION}" -Dplay.home=`dirname $0` -Dsbt.boot.properties=`dirname $0`/sbt/sbt.boot.properties -Dsbt.scala.version=${SBT_SCALA_VERSION} ${PLAY_OPTS} -jar `dirname $0`/sbt/sbt-launch.jar "$@" # playの再起動 $ cd helloworld $ play run --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Ctrl+D to stop and go back to the console...) [info] play - Application started (Dev) $ sudo vim /etc/sysconfig/iptables # 追加 -A INPUT -m state --state NEW -m tcp -p tcp --dport 9000 -j ACCEPT $ sudo service iptables restart
Practice
JavaHome(日本語)
JavaHome(English)目的
以下ではDBからデータを引っ張ってViewに表示するまでの一連の処理をJava Play Frameworkで記載します。
File layout
appの下にMVCのロジックを書くようですが、本家のドキュメントにあるようなmodelsディレクトリが生成されません。ちなみにですが、日本語のplay framewokのドキュメントは2.1.5までのversionしかないので、新しいドキュメントを参照したい場合は本家を見た方が良いです。modelsディレクトリは自分でapp以下に付け足して以下のlayoutにします。また後で追加するjar系のlibraryを配置するlibも無いので追加します。
$ pwd /home/yuta/work/src/play/play-2.2.2/helloworld $ tree -L 2 ├── README ├── app │ ├── controllers │ ├── models │ └── views ├── build.sbt ├── conf │ ├── application.conf │ └── routes ├── lib ├── logs │ └── application.log ├── project │ ├── Build.scala │ ├── build.properties │ ├── plugins.sbt │ ├── project │ └── target ├── public │ ├── images │ ├── javascripts │ └── stylesheets ├── target │ ├── native_libraries │ ├── resolution-cache │ ├── scala-2.10 │ └── streams └── test ├── ApplicationTest.java └── IntegrationTest.javamysqlの設定
お試しなので簡単にid,titile,created_at,updated_atのカラムを持つテーブルを定義し、2つデータをINSERTします。
DROP TABLE helloworld.hello; CREATE TABLE helloworld.hello( id INT(11) NOT NULL AUTO_INCREMENT, title VARCHAR(64) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id) ); INSERT INTO helloworld.hello(title,created_at,updated_at) VALUES('This is Hello World!', NOW(), NOW()); INSERT INTO helloworld.hello(title,created_at,updated_at) VALUES('World is not Enough!', NOW(), NOW());Modelの用意
上のTableと接続するDBModelをmodels/Hello.javaという名前で保存します。一般的なDBModelでpropertyにカラム名、メソッドにデータやり取りの関数を記載します。またここでは紹介しませんがJavaのコードで良く見かけるConnection connection = DB.getConnection();connection.createStatement().execute();のように手続き型でもDBからデータを取得する事は可能なようです。
package models; import java.util.*; import play.db.ebean.*; import play.data.validation.Constraints.*; import javax.persistence.*; @Entity public class Hello extends Model { @Id public Integer id; @Column public String title; @Column public Date created_at; @Column public Date updated_at; public static Finder<Long,Hello> find = new Finder(Long.class, Hello.class); public static List<Hello> all() { return find.all(); } }mysql driver
Play FrameworkのdefaultでのDBEngineはh2になっているのでmysqlに変更します。conf/application.confの設定を以下のように変更します。またproject/Build.scalaというファイルを作成しmysql-connector-javaを依存として記述するとplayの再起動時にdownloadしてくれます。ただしdownload先がProjectの一つ上のディレクトリのrepositoryに配置されるので、lib以下にコピーします。準備が整ったらplay runにて再起動をしてアクセスをしてみます。Database 'default' needs evolution!というerror画面が出てもApply This Script Now!というボタンを押下し、更に先の画面でMart it resolvedというボタンを押下すれば処理が先に進みます。
$ vi conf/application.conf db.default.driver=com.mysql.jdbc.Driver db.default.url="jdbc:mysql://localhost/helloworld?characterEncoding=UTF8" db.default.user=root db.default.password="" ebean.default="models.*" $ vi project/Build.scala import sbt._ import Keys._ import play.Project._ object ApplicationBuild extends Build { val appName = "helloworld" val appVersion = "1.0-SNAPSHOT" val appDependencies = Seq( // Add your project dependencies here, javaCore, javaJdbc, javaEbean, "mysql" % "mysql-connector-java" % "5.1.20" ) val subProject = Project("subProject",file("subProject-dir")) val main = play.Project(appName, appVersion, appDependencies, path = file("playProject")) .dependsOn(subProject) } $ cp ../repository/cache/mysql/mysql-connector-java/jars/mysql-connector-java-5.1.20.jar lib/ $ play run --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Ctrl+D to stop and go back to the console...) [info] Updating {file:/home/yuta/work/src/play/play-2.2.2/helloworld/}subProject... [info] Resolving org.fusesource.jansi#jansi;1.4 ... [info] Done updating. [info] Updating {file:/home/yuta/work/src/play/play-2.2.2/helloworld/}helloworld... [info] Resolving org.fusesource.jansi#jansi;1.4 ... [info] downloading http://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.20/mysql-connector-java-5.1.20.jar ... [info] [SUCCESSFUL ] mysql#mysql-connector-java;5.1.20!mysql-connector-java.jar (3416ms) [info] Done updating. [error] application - ! @6hlo1ai71 - Internal server error, for (GET) [/] -> play.api.db.evolutions.InvalidDatabaseRevision: Database 'default' needs evolution![An SQL script need to be run on your database.] (略) ! @6hlo29f7h - Internal server error, for (GET) [/@evolutions/apply/default?redirect=http%3A%2F%2Flocalhost%3A9000%2F] -> play.api.db.evolutions.InconsistentDatabase: Database 'default' is in an inconsistent state![An evolution has not been applied properly. Please check the problem and resolve it manually before marking it as resolved.]Controller
Controllerのメソッド呼び出しはconf/routesで決定されます。GET / controllers.Application.index()という記述が/にアクセスした時にcontrollersのApplicationクラスのindexメソッドを呼び出すという定義です。Frameworkによくあるお決まりのやつですね。上で定義したHelloModelのallメソッドを呼び出してDBに格納されているデータの一覧を取得してViewに渡します。return ok();の記述でStatus:200OKを返すようです。okの中のhelloworld.render(hello)でhelloworldのviewに渡すデータを書きます。ここではList型を渡します。
package controllers; import java.util.*; import play.*; import views.html.*; import models.*; public class Application extends Controller { public static Result index() { List<Hello> hello = Hello.all(); return ok(helloworld.render(hello)); } }View
Play FrameworkはMVCのMCはJavaで書けてもViewだけはScala文法なのでそこだけは新しく勉強が必要ですが、記述がとても簡単なので難なくこなせると思います。htmlタグの中への記述もとてもSimpleに書けて楽しいです。下のファイルをviews/helloworld.scala.htmlという名前で保存します。これでDBに入れたデータが表示できるので一通りModel=>Controller=>Viewへのデータのやり取りを記載する事ができました。
@(data: List[Hello]) <!DOCTYPE html> <html> <head></head> <body> @for(node <- data) { <p>id: @node.id</p> <p>title:@node.title</p> <p>created_at:@node.created_at</p> <p>updated_at:@node.updated_at</p> } </body> </html>
Performance
php
Frameworkを使わずにmysqlに接続してデータを取得する処理をScriptとして記載したものをabスクリプトで実行した結果です。rpsは176.39となりました。
<!DOCTYPE html> <html> <head></head> <body> <?php $link = mysqli_connect('localhost', 'root', '', 'helloworld'); $query = 'SELECT id,title,created_at,updated_at FROM hello'; $result = $link->query($query); while($row = mysqli_fetch_array($result)) { echo '<p>' . $row['id'] . '</p><br/>'; echo '<p>' . $row['title'] . '</p><br/>'; echo '<p>' . $row['created_at'] . '</p><br/>'; echo '<p>' . $row['updated_at'] . '</p><br/>'; } mysqli_close($link); ?> </body> </html>$ ab -n 50000 -c 100 "http://localhost/helloworld.php" Server Software: Apache/2.2.15 Server Hostname: localhost Server Port: 80 Document Path: /helloworld.php Document Length: 274 bytes Concurrency Level: 100 Time taken for tests: 283.461 seconds Complete requests: 50000 Failed requests: 0 Write errors: 0 Total transferred: 23414976 bytes HTML transferred: 13708768 bytes Requests per second: 176.39 [#/sec] (mean) Time per request: 566.922 [ms] (mean) Time per request: 5.669 [ms] (mean, across all concurrent requests) Transfer rate: 80.67 [Kbytes/sec] receivedJava Play Framework
上で設定したJava Play FrameworkのDB接続をそのまま利用してPerfomanceを測定。rpsは432となり、Java Play Frameworkの勝利です。2.5倍ほどの実力差がでましたね。(※Play Frameworkの設定を良く理解していないのでもしかしたら有利な条件になっているかもしれないです。)
$ ab -n 50000 -c 100 "http://localhost:9000/" Server Software: Server Hostname: localhost Server Port: 9000 Document Path: / Document Length: 382 bytes Concurrency Level: 100 Time taken for tests: 115.527 seconds Complete requests: 50000 Failed requests: 0 Write errors: 0 Total transferred: 23100000 bytes HTML transferred: 19100000 bytes Requests per second: 432.80 [#/sec] (mean) Time per request: 231.055 [ms] (mean) Time per request: 2.311 [ms] (mean, across all concurrent requests) Transfer rate: 195.27 [Kbytes/sec] received