この記事はScala Advent Calendar 2019の19日目の記事です。
昨日は@taketoraさんの「ScalaのCompilerについて」でした。
明日は@jooohn1234さんの「Scalaで参照透過に作用を扱う」です。
はじめに
先日、Lightbend社が提唱しているServerless2.0/Stateful Serverlessというワードに興味を持ち、Cloudstateのリポジトリを眺めていたときにgraal-akka-httpやgraal-akka-streamというライブラリが使われており、少し調査したところakka-graal-nativeというものを発見しました。
AkkaをGraalVM native-imageでネイティブイメージ化するのはなかなか困難そうだと思っていたので、後日触ってみようと思っていたところ、GraalVM開発者の来日にあわせたGraalVM for Scalaのイベントが開催されるという情報をキャッチし、よし、せっかくだから先に独り入門しておこう!(ただしnative-imageのみ)と思い諸々触ったことを書きたいと思います。
なお、イベントは都合がつかず参加できませんでした...
みなさん代わりに是非ご参加ください。
GraalVM native-imageについて
GraalVMは、GraalというJITコンパイラとTruffleを中心とした多言語実行環境です。
そのなかにnative-imageという、JavaアプリケーションをSubstrateVMというAOT(ahead-of-time)コンパイラでネイティブイメージ化するツールが含まれています。
これを使えば、Javaアプリケーションを実行可能なバイナリにでき、起動時間の短縮やメモリフットプリントの削減をおこなうことができます。
MicronautやQuarkusなど最近のJavaアプリケーションフレームワークは、これを使ってアプリケーションをネイティブイメージ化し、Cloud Native Applicationを構築できるような機能を提供しています。
ServerlessやMicroServiceが、FaaSであったりコンテナベースランタイムであったりを主戦場としている、昨今の業界のトレンドが反映されているのでしょう。
akka-graal-nativeとは
GraalVM native-imageを使えばどんなJavaアプリケーションでもネイティブイメージ化できるかというと、そういうわけではなく、様々な制限があります。
特に、Java Native Interface(JNI)、Javaリフレクション、動的プロキシオブジェクト(java.lang.reflect.Proxy)、クラスパスリソース(Class.getResource)が使用されていると、SubstrateVMはコードの分析ができなくなるようです。
これを解決するためにそれぞれに対応した構成ファイルを作成することで、SubstrateVMはそれらのネイティブイメージ化を行うことができるようです。
SubstrateVM Assisted Configuration of Native Image Builds
Akkaも内部的にリフレクション等を使っている部分があり、そのままではネイティブイメージ化ができないようです。
akka-graal-configは、そのAkka固有の構成ファイルを含んだJarを公開しており、akka-graal-nativeはそれを使って、Akka HTTPのWebアプリケーションをネイティブイメージ化する、サンプル的なプロジェクトになります。
AkkaをつかったScalaアプリケーションをネイティブイメージ化できる...ワクワクしますね。
まずは触ってみる
今回このakka-graal-nativeを弄って動かすためにforkして修正することにしました。
修正した内容は下記のブランチにあります。
https://github.com/hayasshi/akka-graal-native/tree/add-circe
https://github.com/hayasshi/akka-graal-native/tree/add-scalikejdbc
環境構築
基本的には、akka-graal-nativeのREADME.mdに記載されている通りに構築します。
sbtは割愛するとして、まずはGraalVMをいれます。
akka-graal-nativeは、GraalVM CE 19.1.1
で動作確認をおこなっているようです。
このバージョンのGraalVMはJDK8をターゲットとしているため、JDK8を利用する必要があります。
今回はAdoptOpenJDKのjdk8u232-b09を利用しました。
$ java -version
openjdk version "1.8.0_232"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_232-b09)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.232-b09, mixed mode)
GraalVM CE 19.1.1もダウンロードしてきます。
私はMacを使っているので、darwin用のバイナリを任意のパスに展開し、README.mdにあるように環境変数を設定しました。
export GRAAL_HOME=/path/to/graalvm-ce-19.1.1/Contents/Home
export PATH=$PATH:${GRAAL_HOME}/bin
環境変数PATH
に通すことで、gu
(GraalVM Component Updater)が使えるようになります。
$ gu
GraalVM Component Updater v2.0.0
Usage:
...
これを使い、native-imageをインストールします。
$ gu install native-image
Downloading: Component catalog from www.graalvm.org
...
$ gu list
ComponentId Version Component name Origin
--------------------------------------------------------------------------------
graalvm 19.1.1 GraalVM Core
native-image 19.1.1 Native Image github.com
これで準備が整いました。
akka-graal-nativeの構成を確認する
akka-graal-nativeは
- Akka向けのGraalVM native-image用の構成ファイルを保持するakka-graal-configの各ビルドに依存している
- アプリケーションが利用するログなどの依存向けのGraalVM native-image用の構成ファイルを持っている
- sbt-native-packagerを経由してGraalVM native-imageを実行している
上記によってsbtのgraalvm-native-image:packageBin
タスクを実行することでアプリケーションのネイティブイメージを生成することができます。
もちろん、通常のScalaアプリケーションとしてsbt run
させることも可能です。
動かしてみる
このあたりもREADME.md通りに進めます。
まずビルドします。
$ sbt clean graalvm-native-image:packageBin
[info] Loading settings for project global-plugins from metals.sbt ...
[info] Loading global plugins from /path/to/.sbt/1.0/plugins
[info] Loading settings for project akka-graal-native-build from plugins.sbt ...
[info] Loading project definition from /path/to/akka-graal-native/project
[info] Loading settings for project akka-graal-native from build.sbt ...
[info] Set current project to akka-graal-native (in build file:/path/to/akka-graal-native/)
[success] Total time: 0 s, completed 2019/12/14 23:30:38
[info] Updating ...
[info] Done updating.
[warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings.
[info] Compiling 1 Scala source to /path/to/akka-graal-native/target/scala-2.13/classes ...
[info] Done compiling.
[info] Packaging /path/to/akka-graal-native/target/scala-2.13/akka-graal-native_2.13-0.1.jar ...
[info] Done packaging.
[info] Build on Server(pid: 82106, port: 62604)
[info] [akka-graal-native:82106] classlist: 7,531.53 ms
[info] [akka-graal-native:82106] (cap): 2,180.43 ms
[info] [akka-graal-native:82106] setup: 3,072.25 ms
[info] [akka-graal-native:82106] (typeflow): 39,385.95 ms
[info] [akka-graal-native:82106] (objects): 19,459.29 ms
[info] [akka-graal-native:82106] (features): 1,473.52 ms
[info] [akka-graal-native:82106] analysis: 63,382.67 ms
[info] [akka-graal-native:82106] (clinit): 1,949.94 ms
[info] [akka-graal-native:82106] universe: 3,602.94 ms
[info] [akka-graal-native:82106] (parse): 5,031.68 ms
[info] [akka-graal-native:82106] (inline): 10,274.71 ms
[info] [akka-graal-native:82106] (compile): 25,914.78 ms
[info] [akka-graal-native:82106] compile: 43,251.92 ms
[info] [akka-graal-native:82106] image: 3,275.25 ms
[info] [akka-graal-native:82106] write: 1,030.04 ms
[info] [akka-graal-native:82106] [total]: 125,367.25 ms
[success] Total time: 140 s, completed 2019/12/14 23:32:59
Scalaのビルドに加えAOTコンパイルするので、プログラムの内容の割に時間がかかります。
生成物がtarget以下に作られるので、実行します。
$ ./target/graalvm-native-image/akka-graal-native -Djava.library.path=${GRAAL_HOME}/jre/lib
Dec 14, 2019 11:49:46 PM akka.event.slf4j.Slf4jLogger$$anonfun$receive$1 applyOrElse
INFO: Slf4jLogger started
Dec 14, 2019 11:49:46 PM akka.event.slf4j.Slf4jLogger$$anonfun$receive$1 $anonfun$applyOrElse$3
INFO: ssl-config.default is true, using the JDK's default SSLContext
Dec 14, 2019 11:49:46 PM akka.event.slf4j.Slf4jLogger$$anonfun$receive$1 $anonfun$applyOrElse$2
WARNING: You are using ssl-config.default=true and have a weak certificate in your default trust store! (You can modify akka.ssl-config.disabledKeyAlgorithms to remove this message.) WARNING arguments left: 1
Dec 14, 2019 11:49:46 PM akka.event.slf4j.Slf4jLogger$$anonfun$receive$1 $anonfun$applyOrElse$2
WARNING: You are using ssl-config.default=true and have a weak certificate in your default trust store! (You can modify akka.ssl-config.disabledKeyAlgorithms to remove this message.) WARNING arguments left: 1
Dec 14, 2019 11:49:46 PM akka.event.slf4j.Slf4jLogger$$anonfun$receive$1 $anonfun$applyOrElse$2
WARNING: You are using ssl-config.default=true and have a weak certificate in your default trust store! (You can modify akka.ssl-config.disabledKeyAlgorithms to remove this message.) WARNING arguments left: 1
Dec 14, 2019 11:49:46 PM akka.event.slf4j.Slf4jLogger$$anonfun$receive$1 $anonfun$applyOrElse$2
WARNING: You are using ssl-config.default=true and have a weak certificate in your default trust store! (You can modify akka.ssl-config.disabledKeyAlgorithms to remove this message.) WARNING arguments left: 1
Dec 14, 2019 11:49:46 PM akka.event.slf4j.Slf4jLogger$$anonfun$receive$1 $anonfun$applyOrElse$2
WARNING: You are using ssl-config.default=true and have a weak certificate in your default trust store! (You can modify akka.ssl-config.disabledKeyAlgorithms to remove this message.) WARNING arguments left: 1
Dec 14, 2019 11:49:46 PM com.github.vmencik.akkanative.Main$$anonfun$main$6 applyOrElse
INFO: Listening at /0:0:0:0:0:0:0:0:8086
爆速で起動しました...
表示されているWARNINGは、プロダクションで使う場合には調整が必要そうな内容に見えます。参考
Port 8086のようなので、cURLでアクセスしてみます。
$ curl -v http://127.0.0.1:8086/graal-hp-size
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8086 (#0)
> GET /graal-hp-size HTTP/1.1
> Host: 127.0.0.1:8086
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: akka-http/10.1.8
< Date: Sat, 14 Dec 2019 14:54:22 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 5
<
* Connection #0 to host 127.0.0.1 left intact
69697
動いたァァァ!!!
README.mdに書いてあるとおり進めるだけで簡単にビルドできました。
ついでにローカルマシンのネットワークを切断して実行したら、正しくエラーとなりました。スタックトレースもしっかり出ています。
$ curl -v http://127.0.0.1:8086/graal-hp-size
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8086 (#0)
> GET /graal-hp-size HTTP/1.1
> Host: 127.0.0.1:8086
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< Server: akka-http/10.1.8
< Date: Sat, 14 Dec 2019 15:00:59 GMT
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 35
<
* Connection #0 to host 127.0.0.1 left intact
There was an internal server error.
SEVERE: Error during processing of request: 'Tcp command [Connect(www.graalvm.org:443,None,List(),Some(10 seconds),true)] failed because of java.net.UnknownHostException: www.graalvm.org'. Completing with 500 Internal Server Error response. To change default exception handling behavior, provide a custom ExceptionHandler.
akka.stream.StreamTcpException: Tcp command [Connect(www.graalvm.org:443,None,List(),Some(10 seconds),true)] failed because of java.net.UnknownHostException: www.graalvm.org
Caused by: java.net.UnknownHostException: www.graalvm.org
at akka.io.Dns$Resolved.addr(Dns.scala:56)
at akka.io.TcpOutgoingConnection$$anonfun$receive$1.$anonfun$applyOrElse$1(TcpOutgoingConnection.scala:68)
at akka.io.TcpOutgoingConnection.akka$io$TcpOutgoingConnection$$reportConnectFailure(TcpOutgoingConnection.scala:50)
at akka.io.TcpOutgoingConnection$$anonfun$receive$1.applyOrElse(TcpOutgoingConnection.scala:62)
at akka.actor.Actor.aroundReceive(Actor.scala:539)
at akka.actor.Actor.aroundReceive$(Actor.scala:537)
at akka.io.TcpConnection.aroundReceive(TcpConnection.scala:32)
at akka.actor.ActorCell.receiveMessage(ActorCell.scala:612)
at akka.actor.ActorCell.invoke(ActorCell.scala:581)
at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:268)
at akka.dispatch.Mailbox.run(Mailbox.scala:229)
at akka.dispatch.Mailbox.exec(Mailbox.scala:241)
at akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
at com.oracle.svm.core.thread.JavaThreads.threadStartRoutine(JavaThreads.java:460)
at com.oracle.svm.core.posix.thread.PosixJavaThreads.pthreadStartRoutine(PosixJavaThreads.java:193)
しかし、サンプルプロジェクトは往々にして単純なことしかしておらず、Webアプリケーション等でよく使われるようなことをしようとすると、別途構成が必要になるかも知れません。
このサンプルプロジェクトに少し手を加えて動作するかどうか、確認したいと思います。
よく使うライブラリを組み込む
circe + akka-http-circe
WebAPIを実装する場合、jsonでやり取りをしたいことが多いと思われますが、個人的によく使っているライブラリの組み合わせを追加した場合どうなるか確認したいと思います。
依存を追加し、
val circeVersion = "0.12.1"
libraryDependencies ++= Seq(
"de.heikoseeberger" %% "akka-http-circe" % "1.29.1",
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion
)
コードを修正します。
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._
case class Response(size: String)
val route =
path("graal-hp-size") {
get {
onSuccess(graalHomepageSize) { size =>
complete(Response(size.toString))
}
}
}
ビルドして実行し、動作確認します。
$ curl -v http://127.0.0.1:8086/graal-hp-size
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8086 (#0)
> GET /graal-hp-size HTTP/1.1
> Host: 127.0.0.1:8086
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: akka-http/10.1.10
< Date: Sat, 14 Dec 2019 15:30:23 GMT
< Content-Type: application/json
< Content-Length: 16
<
* Connection #0 to host 127.0.0.1 left intact
{"size":"69697"}
動いたァァァ!!!
無事に動きましたが、ネイティブイメージ作成中にWARNINGが出ていました。
おそらく、circeのimportあたりだと思われますが、何が問題になっているかまでは把握できていません。
[info] Build on Server(pid: 82106, port: 62604)
[info] [akka-graal-native:82106] classlist: 9,457.40 ms
[info] [akka-graal-native:82106] (cap): 1,203.96 ms
[info] [akka-graal-native:82106] setup: 1,488.90 ms
[error] warning: unknown locality of class Lcom/github/vmencik/akkanative/Main$anon$importedEncoder$macro$15$1;, assuming class is not local. To remove the warning report an issue to the library or language author. The issue is caused by Lcom/github/vmencik/akkanative/Main$anon$importedEncoder$macro$15$1; which is not following the naming convention.
[info] [akka-graal-native:82106] (typeflow): 26,753.36 ms
[info] [akka-graal-native:82106] (objects): 19,006.23 ms
[info] [akka-graal-native:82106] (features): 1,798.85 ms
[info] [akka-graal-native:82106] analysis: 49,784.60 ms
[info] [akka-graal-native:82106] (clinit): 1,265.02 ms
[info] [akka-graal-native:82106] universe: 2,402.96 ms
[info] [akka-graal-native:82106] (parse): 3,650.25 ms
[info] [akka-graal-native:82106] (inline): 5,861.99 ms
[info] [akka-graal-native:82106] (compile): 21,919.23 ms
[info] [akka-graal-native:82106] compile: 33,162.10 ms
[info] [akka-graal-native:82106] image: 2,635.47 ms
[info] [akka-graal-native:82106] write: 989.83 ms
[info] [akka-graal-native:82106] [total]: 100,021.03 ms
JDBC
RDBへのアクセスはまだまだ多いと思い、JDBCをつかったDB接続ができないか試してみました。
MySQLを使うことが多いので色々調べていたのですが、どうやらMySQLはJDBCドライバーであるmysql-connector-java
がバグにより動作しないという情報がありました。
このissueにMariaDB Java Clientなら動作したという情報があったので、同じような構成で色々試してみましたが、結局エラーでビルドできませんでした。
入門したてではこのあたりが限界でした。
[info] Build on Server(pid: 17904, port: 57210)
[info] [akka-graal-native:17904] classlist: 17,549.63 ms
[info] [akka-graal-native:17904] (cap): 1,419.75 ms
[info] [akka-graal-native:17904] setup: 1,915.65 ms
[info] [akka-graal-native:17904] analysis: 19,051.22 ms
[error] Error: Error encountered while parsing org.mariadb.jdbc.internal.util.Utils.retrieveProxy(org.mariadb.jdbc.UrlParser, org.mariadb.jdbc.internal.util.pool.GlobalStateInfo)
[error] Parsing context:
[error] parsing org.mariadb.jdbc.MariaDbConnection.newConnection(MariaDbConnection.java:142)
[error] parsing org.mariadb.jdbc.Driver.connect(Driver.java:86)
[error] parsing java.sql.DriverManager.getConnection(DriverManager.java:664)
[error] parsing java.sql.DriverManager.getConnection(DriverManager.java:208)
[error] parsing org.apache.commons.dbcp2.DriverManagerConnectionFactory.createConnection(DriverManagerConnectionFactory.java:123)
[error] parsing org.apache.commons.dbcp2.PoolableConnectionFactory.makeObject(PoolableConnectionFactory.java:355)
[error] parsing org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:889)
[error] parsing org.apache.commons.pool2.impl.GenericObjectPool.destroy(GenericObjectPool.java:939)
[error] parsing org.apache.commons.pool2.impl.GenericObjectPool.clear(GenericObjectPool.java:648)
[error] parsing org.apache.commons.pool2.impl.GenericObjectPool.close(GenericObjectPool.java:692)
[error] parsing java.io.FileDescriptor.closeAll(FileDescriptor.java:202)
[error] parsing java.io.FileInputStream.close(FileInputStream.java:334)
[error] parsing java.util.logging.LogManager.readConfiguration(LogManager.java:1304)
[error] parsing java.util.logging.LogManager$3.run(LogManager.java:399)
[error] parsing java.util.logging.LogManager$3.run(LogManager.java:396)
[error] parsing com.oracle.svm.core.jdk.Target_java_security_AccessController.doPrivileged(SecuritySubstitutions.java:83)
[error] parsing java.util.logging.LogManager.readPrimordialConfiguration(LogManager.java:396)
[error] parsing java.util.logging.LogManager.access$800(LogManager.java:145)
[error] parsing java.util.logging.LogManager$2.run(LogManager.java:345)
[error] parsing com.oracle.svm.core.jdk.Target_java_security_AccessController.doPrivileged(SecuritySubstitutions.java:63)
[error] parsing java.util.logging.LogManager.ensureLogManagerInitialized(LogManager.java:338)
[error] parsing java.util.logging.LogManager.getLogManager(LogManager.java:378)
[error] parsing com.github.vmencik.akkanative.Main$.configureLogging(Main.scala:97)
[error] parsing com.github.vmencik.akkanative.Main$.main(Main.scala:33)
[error] parsing com.github.vmencik.akkanative.Main.main(Main.scala)
[error] parsing com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:147)
[error] parsing com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)
[error] Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
[error] Error: Image build request failed with exit status 1
まとめと所感
akka-graal-configを使えば、Akkaに依存したアプリケーションをネイティブイメージ化することが可能になりました。
Akka Actor, Streams, Httpなどをつかい、そこそこの規模のアプリケーションをネイティブイメージ化させることも可能そうに見えました。
ただし、GraalVMのドキュメントに下記のような警告もあるように、プロダクション環境で利用していくには色々と踏み抜いて進んでいく覚悟が必要そうです。
しばらくは限定的な用途にとどまりそうと感じました。
Warning: GraalVM Native Image is available as an Early Adopter technology.
ネイティブイメージのビルドに関しても、開発者がアプリケーションに依存するライブラリの中身まですべて把握して、構成ファイルを準備するのはハードルがまだ高そうに感じます。(native-image-agentというものもあるが、それでも)
ただし、ビルドするアプリケーションのクラスパスに構成ファイルとそれへのパスが記載されているnative-image.properties
が含まれていれば、構成を自動で検知してくれるということ(参考)なので、SubstrateVM向けの構成ファイルの提供をライブラリ側でやってもらえると、今後もっと楽にネイティブイメージ化ができていきそうだなと感じました。
(もちろん、ライブラリ開発者の負担が高くなるので、GraalVMでのネイティブイメージ化がより使われていかなければ、なかなか広がらないとは思います)
JavaやJVM同様、GraalVMも活発に開発されており、今後もより使いやすく、より改善や機能開発が行われていくと思いますので、Akka含めて引き続きウォッチしていきたいと思います。