Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

速いよ Java Play Framework

言語とFrameworkの選定

phpにはあまり魅力を感じていない@yutakikuchi_です。本題とは関係ありませんが4.25(金)@ヒカリエのイベントに登壇します。ネタは同窓会GrowthHackとログ集計/解析の2本立てです。興味のある方はどうぞ。【ヒカ☆ラボ】同窓会GrowthHack!×データログ集計、解析!をテーマに事例をまじえお話します! 16年ぶりの再会でも参加率6割の同窓会を開くには?Yahoo出身のエンジニアが語る、アクセスログ可視化、 ユーザ属性解析を行うためのシステム設計のコツとは? はてなブックマーク - 【ヒカ☆ラボ】同窓会GrowthHack!×データログ集計、解析!をテーマに事例をまじえお話します! 16年ぶりの再会でも参加率6割の同窓会を開くには?Yahoo出身のエンジニアが語る、アクセスログ可視化、 ユーザ属性解析を行うためのシステム設計のコツとは?


Round 8 results - TechEmpower Framework Benchmarks はてなブックマーク - 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、Javajava-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/javac
installと実行
$ 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
JavaHome(English) はてなブックマーク - JavaHome

目的

以下では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.java
mysqlの設定

お試しなので簡単に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] received
Java 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