Netty HTTP HelloWorldの起動が600msくらいだったんだけどGraalVMのNative Imageを使うと15msくらいになったー

下記の記事を読んで「えー。実際に手を動かして、自分の目で見てみたいー。」ってなったので、書いてある通りにやってみたら10ms前後でほんとに起動した。へー。その途中で色々と知らないことがたくさんあって面白かった。

medium.com

## やったことを順番に書いていく

GraalVMでNative Imageを作るまでに色々やらなきゃいけないので、やったことを順番に書いていく。ソースコードはここに置いといて、順番にタグ付けしておいた。

github.com

使ったのは、GraalVM 1.0-RC15。SDKMAN!で指定した。

❯ sdk current java

Using java version 1.0.0-rc-15-grl

## v1 サンプルプロジェクトを作成

記事にもある通り Netty Exampleの中のHTTPのハローワールドを使ってサンプルプロジェクトを作った。v1がタグね。

maven-assembly-plugin でFatJARを作って実行できるようにしてあるので

      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.1.1</version>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>com.example.demo.helloworld.HttpHelloWorldServer</mainClass>
            </manifest>
          </archive>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

packageすると

❯ ./mvnw clean package

target ディレクトリーに netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar が生成されるから、それを実行。

❯ java -jar target/netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar
Apr 15, 2019 9:59:48 PM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0x4b3bc1c7] REGISTERED
Apr 15, 2019 9:59:48 PM io.netty.handler.logging.LoggingHandler bind
INFO: [id: 0x4b3bc1c7] BIND: 0.0.0.0/0.0.0.0:8080
Open your web browser and navigate to http://127.0.0.1:8080/
Apr 15, 2019 9:59:48 PM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0x4b3bc1c7, L:/0:0:0:0:0:0:0:0:8080] ACTIVE

普通に起動して、Hello Worldが見える。

f:id:bufferings:20190415220143p:plain:w300

じゃ、ここからGraalVMでNative Imageを生成できるようにしていく。

## v2 native-image-maven-plugin を追加

記事にあるみたいに直接 native-image コマンドを叩いてもいいんだけど、Mavenでビルドできるのも楽そうでいいなと思って導入してみた。先に言っておくと、この時点ではビルドはエラーになる。

      <plugin>
        <groupId>com.oracle.substratevm</groupId>
        <artifactId>native-image-maven-plugin</artifactId>
        <version>1.0.0-rc15</version>
        <configuration>
          <imageName>netty-example-http</imageName>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>native-image</goal>
            </goals>
            <phase>package</phase>
          </execution>
        </executions>
      </plugin>

このプラグインに関しては既に、かずひらさんが書いてくれてた!

❯ ./mvnw clean package

を実行すると、さっきのFatJARが生成された後に、Native Imageのビルドが始まる。で、2種類の警告が出る。

1つ目は sun.misc.Unsafe によるUnsafeなメモリアクセス。 Unsafe を使ってたとしても一般的なパターンで使ってたら native-image がいい感じで対応してくれるんだけど、対応しきれないやつが今回の場合は3つある(最初に書いた参照記事によると、94個はいい感じに対応できてて、3個だけ対応しきれないってことみたい)。

↓適当に改行を入れといた。メッセージが丁寧だね。

Warning: RecomputeFieldValue.ArrayIndexScale automatic substitution failed.
The automatic substitution registration was attempted because a call to sun.misc.Unsafe.arrayIndexScale(Class) was detected
in the static initializer of io.netty.util.internal.PlatformDependent0.
Detailed failure reason(s): The field java.lang.Long.value, where the value produced by the array index scale computation is stored, is not static.                                                                                                                                                                                  

Warning: RecomputeFieldValue.FieldOffset automatic substitution failed.
The automatic substitution registration was attempted because a call to sun.misc.Unsafe.objectFieldOffset(Field) was detected
in the static initializer of io.netty.util.internal.PlatformDependent0.
Add a RecomputeFieldValue.FieldOffset manual substitution for io.netty.util.internal.PlatformDependent0.ADDRESS_FIELD_OFFSET.
Detailed failure reason(s): The argument of Unsafe.objectFieldOffset(Field) is not a constant field.                                                                                                     

Warning: RecomputeFieldValue.ArrayIndexScale automatic substitution failed.
The automatic substitution registration was attempted because a call to sun.misc.Unsafe.arrayIndexScale(Class) was detected
in the static initializer of io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess.
Detailed failure reason(s): Could not determine the field where the value produced by the call to sun.misc.Unsafe.arrayIndexScale(Class) for the array index scale computation is stored.
The call is not directly followed by a field store or by a sign extend node followed directly by a field store.  

それともう1つはLogger周り。ロガーはよく「実行時にあるやつを使う」みたいな実装がしてあって、Nettyだと InternalLoggerFactory がそういう実装になってる。 けど、Substrate VM(Native Imageで使われるVM実装)で動かすときはビルド時にクラスが存在しないといけない。だからこんな感じの警告(エラー)がでる。

Warning: Abort stand-alone image build. unsupported features in 5 methods                                                                                                                              
Detailed message:                                                                                                                                                                                      
Error: Class initialization failed: io.netty.util.internal.logging.Log4JLogger                                                                                                                         
Original exception that caused the problem: java.lang.NoClassDefFoundError: org/apache/log4j/Priority 

Error: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: io.netty.util.internal.logging.Log4J2Logger.
To diagnose the issue you can use the --allow-incomplete-classpath option. The missing type is then reported at run time when it is accessed the first time.
Trace:
        at parsing io.netty.util.internal.logging.Log4J2LoggerFactory.newInstance(Log4J2LoggerFactory.java:33)
Call path from entry point to io.netty.util.internal.logging.Log4J2LoggerFactory.newInstance(String):
        at io.netty.util.internal.logging.Log4J2LoggerFactory.newInstance(Log4J2LoggerFactory.java:33)
        at io.netty.util.internal.logging.InternalLoggerFactory.getInstance(InternalLoggerFactory.java:93)

...

## v3 Unsafe 対応

Unsafe の対応として、Substrate VMのSubstitution(以下、代用)機能を使うので svm への依存を追加。

    <dependency>
      <groupId>com.oracle.substratevm</groupId>
      <artifactId>svm</artifactId>
      <version>1.0.0-rc15</version>
      <scope>provided</scope>
    </dependency>

そして、代用機能のためのクラスを作成。

package com.example.demo.substitution;

import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.TargetClass;

import static com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;

@TargetClass(className = "io.netty.util.internal.PlatformDependent0")
final class Target_io_netty_util_internal_PlatformDependent0 {
  @Alias
  @RecomputeFieldValue(kind = Kind.FieldOffset, declClassName = "java.nio.Buffer", name = "address")
  private static long ADDRESS_FIELD_OFFSET;
}

@TargetClass(className = "io.netty.util.internal.CleanerJava6")
final class Target_io_netty_util_internal_CleanerJava6 {
  @Alias
  @RecomputeFieldValue(kind = Kind.FieldOffset, declClassName = "java.nio.DirectByteBuffer", name = "cleaner")
  private static long CLEANER_FIELD_OFFSET;
}

@TargetClass(className = "io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess")
final class Target_io_netty_util_internal_shaded_org_jctools_util_UnsafeRefArrayAccess {
  @Alias
  @RecomputeFieldValue(kind = Kind.ArrayIndexShift, declClass = Object[].class)
  public static int REF_ELEMENT_SHIFT;
}

public class NettySubstitution {
}

これは元記事のサンプルコードをコピペしただけで、よく分かってない。自分で書けと言われてもまだ書けない。

ロガーの問題はまだ解決してないけど、とりあえずこの時点でビルドしてみるとどうなってるかな?

❯ ./mvnw clean package

お。 Unsafe の警告が消えて、ロガーのエラーだけになってた。よし。

## v4 ロガー対応

さっきの代用機能のためのクラスに下記のクラスを追記

@TargetClass(io.netty.util.internal.logging.InternalLoggerFactory.class)
final class Target_io_netty_util_internal_logging_InternalLoggerFactory {
  @Substitute
  private static InternalLoggerFactory newDefaultFactory(String name) {
    return JdkLoggerFactory.INSTANCE;
  }
}

これは InternalLoggerFactory クラスの newDefaultFactory メソッドが JdkLoggerFactoryインスタンスを返すようにしてるってことなんだろうな。

元々は実行時にクラスの有無を見て、Log4JだとかLog4J2だとかのFactoryを返す実装になってるところを、存在するやつを固定で返すようにしてるってことか。ふむふむ。

さて、これでビルド通るかな?

❯ ./mvnw clean package

お、警告も、フォールバック的なメッセージもなく、ビルドに成功した。 target ディレクトリーの下に netty-example-http って名前(pomで指定した名前)で実行可能ファイルが生成されてる(あ、いや、実は今までも生成されてたんだけど)。実行してみる。

❯ ./target/netty-example-http
Exception in thread "main" java.lang.IllegalArgumentException: Class NioServerSocketChannel does not have a public non-arg constructor
        at io.netty.channel.ReflectiveChannelFactory.<init>(ReflectiveChannelFactory.java:36)
        at io.netty.bootstrap.AbstractBootstrap.channel(AbstractBootstrap.java:106)
        at com.example.demo.helloworld.HttpHelloWorldServer.main(HttpHelloWorldServer.java:56)
Caused by: java.lang.NoSuchMethodException: io.netty.channel.socket.nio.NioServerSocketChannel.<init>
        at java.lang.Class.getConstructor0(DynamicHub.java:3082)
        at java.lang.Class.getConstructor(DynamicHub.java:1825)
        at io.netty.channel.ReflectiveChannelFactory.<init>(ReflectiveChannelFactory.java:34)
        ... 2 more

(´・ω・`)しょぼーん

## v5 リフレクション対応

原因は HttpHelloWorldServer のこの部分

            b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new HttpHelloWorldServerInitializer(sslCtx));

channelのところで NioServerSocketChannel.class を渡して、このコンストラクターをリフレクションで呼び出してる。

ので、それをSubstrate VMに教えてあげる必要があるらしい。プロジェクト直下に netty_reflection_config.json という名前でこんなファイルを置いて

[
  {
    "name": "io.netty.channel.socket.nio.NioServerSocketChannel",
    "methods": [
      { "name": "<init>", "parameterTypes": [] }
    ]
  }
]

それを使うってことを native-image のパラメーターで渡す必要があるので、pomに書き加えておく。

        <configuration>
          <buildArgs>
            -H:ReflectionConfigurationFiles=${project.basedir}/netty_reflection_config.json
          </buildArgs>
          <imageName>netty-example-http</imageName>
        </configuration>

でビルドしたら・・・別の警告でた。

## v6 遅延初期化対応

Substrate VMはビルド時にクラスの初期化までしてるんだけど、それができないクラスがあるよってことみたい。で、その場合は --delay-class-initialization-to-runtime=<class-name> をつけたら良いよって書いてる。

Warning: Abort stand-alone image build. Detected a direct/mapped ByteBuffer in the image heap.
A direct ByteBuffer has a pointer to unmanaged C memory, and C memory from the image generator is not available at image run time.
A mapped ByteBuffer references a file descriptor, which is no longer open and mapped at run time.
The object was probably created by a class initializer and is reachable from a static field.
By default, all class initialization is done during native image building.
You can manually delay class initialization to image run time by using the option --delay-class-initialization-to-runtime=<class-name>.
Or you can write your own initialization methods and call them explicitly from your main entry point.

Detailed message:
Trace:  object io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf
        object io.netty.buffer.UnreleasableByteBuf
        method io.netty.handler.codec.http.HttpObjectEncoder.encodeChunkedContent(ChannelHandlerContext, Object, long, List)
Call path from entry point to io.netty.handler.codec.http.HttpObjectEncoder.encodeChunkedContent(ChannelHandlerContext, Object, long, List):
...

ということで、pomのbuildArgsにそのパラメーターも追加する。

        <configuration>
          <buildArgs>
            -H:ReflectionConfigurationFiles=${project.basedir}/netty_reflection_config.json
            --delay-class-initialization-to-runtime=io.netty.handler.codec.http.HttpObjectEncoder
          </buildArgs>
          <imageName>netty-example-http</imageName>
        </configuration>

で、ビルドしたら、成功したー(∩´∀`)∩ワーイ

からの、実行も成功したー(∩´∀`)∩ワーイ

f:id:bufferings:20190416001726p:plain:w300

じゃ、速さを比べてみるかなー。

## v7 速度比較

のために「起動できたよ!」のメッセージ直後にexitするようにする。

            System.err.println("Open your web browser and navigate to " +
                (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');
            System.exit(0);

で、ビルドしなおして

❯ ./mvnw clean package

まずはJARで実行

❯ time java -jar target/netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar
Apr 16, 2019 12:22:12 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0xfdacf35e] REGISTERED
Apr 16, 2019 12:22:12 AM io.netty.handler.logging.LoggingHandler bind
INFO: [id: 0xfdacf35e] BIND: 0.0.0.0/0.0.0.0:8080
Open your web browser and navigate to http://127.0.0.1:8080/
Apr 16, 2019 12:22:12 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0xfdacf35e, L:/0:0:0:0:0:0:0:0:8080] ACTIVE
java -jar target/netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar  0.45s user 0.08s system 88% cpu 0.592 total

❯ /usr/bin/time -f "\nmaxRSS\t%MkB" java -jar target/netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar
Apr 16, 2019 12:22:41 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0x6656e56c] REGISTERED
Apr 16, 2019 12:22:41 AM io.netty.handler.logging.LoggingHandler bind
INFO: [id: 0x6656e56c] BIND: 0.0.0.0/0.0.0.0:8080
Open your web browser and navigate to http://127.0.0.1:8080/
Apr 16, 2019 12:22:41 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0x6656e56c, L:/0:0:0:0:0:0:0:0:8080] ACTIVE

maxRSS  105008kB

592msと105MBだね。何度か実行してみてもだいたいこのぐらいだった。

最後に、Native Imageを実行。

❯ time target/netty-example-http
Open your web browser and navigate to http://127.0.0.1:8080/
target/netty-example-http  0.00s user 0.01s system 89% cpu 0.011 total

~/workspace/micronaut/netty-native-demo master*
❯ /usr/bin/time -f "\nmaxRSS\t%MkB" target/netty-example-http
Open your web browser and navigate to http://127.0.0.1:8080/

maxRSS  8700kB

11msと8.7MBだね。何度か実行してみてもだいたいこのぐらいだった。

## 満足した

  • ちょっとだけGraalVMのNative Image周りのことに入門できた
  • 分からないことがいっぱいあった
  • 速かった

面白かったー!

JibでMicronautのアプリをビルドしてGCRにpush

昨日MicronautとDockerを触りながら「そういえばJibってのがあったけど触ってなかったなぁ」と思ったので触ってみた。感想は「気の利くやつだな」。

## Jib?

Googleの提供してるコンテナ化ツール。

github.com

去年の夏頃発表されたんだっけな。

cloud.google.com

通常、Javaのアプリをコンテナ化するときって、例えばSpringBootとかMicronautだと

  1. GradleとかMavenでビルドしてFatJAR(依存ライブラリー全部入りのJARファイル)を作る
  2. そのJARファイルとDockerfileを使ってコンテナをビルドする
  3. レジストリーにpushする

という手順になるのだけど、Jibを使うとDockerfileを書かなくても空気を読んでGradleやMavenでコンテナのビルドやpushまでコマンドひとつでやってくれる。*1

それと、Dockerデーモンがなくてもビルドできるみたい。ローカルだとDockerは入れておくだろうけど、CIサーバーとかだとDockerなくても大丈夫ってのは良さそう。

## MicronautのアプリをGCRにpushしてみる

この辺を読みながらやってみた。

https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin

### build.gradle

まずは、Micronautのプロジェクトを作るところから始めてみた*2

❯ mn create-app hello-jib --features spock                                                                                                                                                             
| Generating Java project...                                                                                                                                                                           
| Application created at /home/bufferings/workspace/micronaut/hello-jib    

で、build.gradleにjibの設定を入れる。プラグインの部分と、生成するイメージの名前。

plugins {
    id "io.spring.dependency-management" version "1.0.6.RELEASE"
    id "com.github.johnrengelman.shadow" version "4.0.2"
    id "application"
    id "groovy"
    id "java"
    id "net.ltgt.apt-eclipse" version "0.21"
    id "net.ltgt.apt-idea" version "0.21"

    // これ
    id 'com.google.cloud.tools.jib' version '1.0.2'
}
// これ
jib.to.image = 'asia.gcr.io/bufferings/my-app'

Google Container Registryにpushしてみようかなと思ったので、イメージ名はそうしといた。

### GCR用のクレデンシャルを用意

Make sure you have the docker-credential-gcr command line tool. Jib automatically uses docker-credential-gcr for obtaining credentials. See Authentication Methods for other ways of authenticating.

って書いてあるので、設定しといた。

❯ docker-credential-gcr configure-docker                                                                                                                                                               
/home/bufferings/.docker/config.json configured to use this credential helper for GCR registries  

### 実行!

のためのGradleタスクはこの2つ

  • ./gradlew jib → ビルドしてレジストリーにpushする
  • ./graldew jibDockerBuild → ローカルのDockerデーモンに対してイメージのビルドをする

とりあえず、前者の ./gradlew jib を実行してみたら、エラーになった。

❯ ./gradlew jib                                                                                                                                                                                        
                                                                                                                                                                                                       
> Task :compileJava                                                                                                                                                                                    
warning: Supported source version 'RELEASE_11' from annotation processor 'org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor' less than -source '12'                               
warning: Supported source version 'RELEASE_11' from annotation processor 'org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor' less than -source '12'                               
warning: Supported source version 'RELEASE_11' from annotation processor 'org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor' less than -source '12'                               
3 warnings                                                                                                                                                                                             
                                                                                                                                                                                                       
> Task :jib FAILED                                                                                                                                                                                     
                                                                                                                                                                                                       
FAILURE: Build failed with an exception.                                                                                                                                                               
                                                                                                                                                                                                       
* What went wrong:                                                                                                                                                                                     
Execution failed for task ':jib'.                                                                                                                                                                      
> Your project is using Java 12 but the base image is for Java 11, perhaps you should configure a Java 12-compatible base image using the 'jib.from.image' parameter, or set targetCompatibility = 11 o
r below in your build configuration                                                                                                                                                                    
                                                                                                                                                                                                       
* Try:                                                                                                                                                                                                 
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 4s
3 actionable tasks: 3 executed

Java 12使ってたけど、Base Imageは11じゃよ。って怒られた。JibはデフォルトでDistroless/javaを使うらしいんだけど、それがJava 11ってことか。そっか。

❯ sdk use java 11.0.2-open

Using java version 11.0.2-open in this shell.

これでいけるかな。

❯ ./gradlew jib
Starting a Gradle Daemon, 1 incompatible and 2 stopped Daemons could not be reused, use --status for details

Containerizing application to asia.gcr.io/bufferings/my-app...

Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, hello.jib.Application]

Built and pushed image as asia.gcr.io/bufferings/my-app
Executing tasks:
[==============================] 100.0% complete


BUILD SUCCESSFUL in 2m 4s
3 actionable tasks: 2 executed, 1 up-to-date

レジストリー見てみたら、ちゃんとpushされてるー。(∩´∀`)∩ワーイ

f:id:bufferings:20190414152207p:plain:w300

### pullしてみるか

じゃ、pullして実行できるの確認しとこかな?と思ったけど、そのまえに、そもそも、実はビルド時にローカルにも登録されてたりしないのかな?と思って見てみた。

❯ docker images | grep gcr
asia.gcr.io/bufferings/quickstart-image   tag1                       e1ddd7948a1c        8 months ago        1.16MB
gcr.io/distroless/java                    latest                     d53055d7a4da        49 years ago        118MB

ないね。じゃ、やってみよ。

❯ docker run --rm -p 8080:8080 asia.gcr.io/bufferings/my-app
Unable to find image 'asia.gcr.io/bufferings/my-app:latest' locally
latest: Pulling from bufferings/my-app
f6045256ec3f: Pull complete
5f5edd681dcb: Pull complete
3e010093287c: Pull complete
e6476c6c5a02: Pull complete
4988a9948f57: Pull complete
ffc771f05eaa: Pull complete
45a6b543af72: Pull complete
Digest: sha256:eeb9e44406fb12dc4ed318c2ca56d36db065a8dbbae32d435526a50e65f33d39
Status: Downloaded newer image for asia.gcr.io/bufferings/my-app:latest
01:30:48.626 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 2115ms. Server Running: http://a1151605f84c:8080
^C01:32:10.753 [Thread-2] INFO  io.micronaut.runtime.Micronaut - Embedded Application shutting down

OK。

## 中身が気になる

中身が気になるなー。どうなってるんだろうなー?

### Entrypoint

まずはEntrypointを見てみた。さっきビルドしたときにも出てきたな。

❯ docker inspect asia.gcr.io/bufferings/my-app | jq '.[] | .Config.Entrypoint'
[
  "java",
  "-cp",
  "/app/resources:/app/classes:/app/libs/*",
  "hello.jib.Application"
]

ふーん。

### 中に入ってみる

Distrolessにはshも入ってないから、デバッグ用に中に入って色々見てみたりしたいならdebugタグを使ってねってFAQに書いてある。

https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#where-is-bash

If you would like to include a shell for debugging, set the base image to gcr.io/distroless/java:debug instead. The shell will be located at /busybox/sh. Note that :debug images are not recommended for production use.

おすおす。こんな感じに書き換えてみた。

    id 'com.google.cloud.tools.jib' version '1.0.2'
}
jib {
    from {
        // これを足した
        image = 'gcr.io/distroless/java:debug'
    }
    to {
        image = 'asia.gcr.io/bufferings/my-app'
    }
}

で、こんどはpushしなくてもいいからローカル用に jibDockerBuild でビルド。したら、エラー。

❯ ./gradlew jibDockerBuild                                                                                                                                                                             
> Task :jibDockerBuild FAILED                                                                                                                                                                          
                                                                                                                                                                                                       
FAILURE: Build failed with an exception.                                                                                                                                                               
                                                                                                                                                                                                       
* What went wrong:                                                                                                                                                                                     
Execution failed for task ':jibDockerBuild'.                                                                                                                                                           
> Your project is using Java 11 but the base image is for Java 8, perhaps you should configure a Java 11-compatible base image using the 'jib.from.image' parameter, or set targetCompatibility = 8 or 
below in your build configuration                                                                                                                                                                      
                                                                                                                                                                                                       
* Try:                                                                                                                                                                                                 
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.                                                   
                                                                                                                                                                                                       
* Get more help at https://help.gradle.org                                                                                                                                                             

BUILD FAILED in 1s
3 actionable tasks: 1 executed, 2 up-to-date

あら。debugタグはJava 8なのか。

❯ sdk use java 8.0.202.hs-adpt

Using java version 8.0.202.hs-adpt in this shell.

8に変えて、ビルド。

❯ ./gradlew jibDockerBuild
Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details

Containerizing application to Docker daemon as asia.gcr.io/bufferings/my-app...

Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, hello.jib.Application]

Built image to Docker daemon as asia.gcr.io/bufferings/my-app
Executing tasks:
[==============================] 100.0% complete


BUILD SUCCESSFUL in 35s
3 actionable tasks: 2 executed, 1 up-to-date

おっけー。

じゃ、中に入ってみてみるかー。

❯ docker run --rm -ti --entrypoint /busybox/sh asia.gcr.io/bufferings/my-app
/ # find /app
/app
/app/libs
/app/libs/netty-codec-4.1.30.Final.jar
/app/libs/netty-transport-4.1.30.Final.jar
/app/libs/micronaut-http-client-1.1.0.jar
/app/libs/jackson-core-2.9.8.jar
/app/libs/micronaut-core-1.1.0.jar
/app/libs/netty-handler-4.1.30.Final.jar
/app/libs/netty-buffer-4.1.30.Final.jar
/app/libs/micronaut-http-1.1.0.jar
/app/libs/micronaut-aop-1.1.0.jar
/app/libs/jackson-databind-2.9.8.jar
/app/libs/netty-codec-socks-4.1.30.Final.jar
/app/libs/netty-handler-proxy-4.1.30.Final.jar
/app/libs/validation-api-2.0.1.Final.jar
/app/libs/javax.annotation-api-1.3.2.jar
/app/libs/jackson-annotations-2.9.8.jar
/app/libs/netty-resolver-4.1.30.Final.jar
/app/libs/jsr305-3.0.2.jar
/app/libs/slf4j-api-1.7.25.jar
/app/libs/micronaut-validation-1.1.0.jar
/app/libs/netty-common-4.1.30.Final.jar
/app/libs/micronaut-http-netty-1.1.0.jar
/app/libs/javax.inject-1.jar
/app/libs/micronaut-http-server-netty-1.1.0.jar
/app/libs/micronaut-runtime-1.1.0.jar
/app/libs/logback-core-1.2.3.jar
/app/libs/jackson-datatype-jsr310-2.9.8.jar
/app/libs/reactive-streams-1.0.2.jar
/app/libs/micronaut-buffer-netty-1.1.0.jar
/app/libs/micronaut-websocket-1.1.0.jar
/app/libs/logback-classic-1.2.3.jar
/app/libs/jackson-datatype-jdk8-2.9.8.jar
/app/libs/rxjava-2.2.6.jar
/app/libs/snakeyaml-1.23.jar
/app/libs/netty-codec-http-4.1.30.Final.jar
/app/libs/micronaut-inject-1.1.0.jar
/app/libs/micronaut-http-server-1.1.0.jar
/app/libs/micronaut-router-1.1.0.jar
/app/classes
/app/classes/hello
/app/classes/hello/jib
/app/classes/hello/jib/Application.class
/app/resources
/app/resources/application.yml
/app/resources/logback.xml

ふーん。libsに依存ライブラリーが入って、classesにクラスファイルが、resourcesにはリソースファイルが入るのか。なるほどなー。気の利くやつだな。

## その他

細かく色々設定できるみたい。

https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin

環境変数とかは普通に外から渡せば良さそう。

https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#how-do-i-set-parameters-for-my-image-at-runtime

おしまい。これくらいメモ残しておけば、また使いたくなったときに思い出せるかな。

*1:だからといって何も中身を知らなくていいという訳ではないよなー

*2:例によってSpockは好みでつけてるだけ

MicronautのGraalVM Native ImageはDocker使うより普通にビルドする方が良さそう

## 2019-04-17 追記

ブログ書くだけなのもあれだなと思って、こんな感じにしたらどうかな?ってIssueを書いた。

github.com

そしたら「1.1.0でnative image化が十分シンプルになったから、ビルド用のシェルスクリプトを削除したんだ」って教えてもらった。まぁ、確かにそうね。

なので、普通にビルドするってことでいいし、Dockerでやりたい場合でも、僕がこのブログ記事で書いてるようなので良さそう。かな。

それと、マルチステージビルドをしっかりやってるサンプルも教えてもらった。ふむふむー。

https://github.com/micronaut-projects/micronaut-gcp/blob/master/examples/hello-world-cloud-run-graal/Dockerfile

追記はここまでです!

## Native Image

MicronautにはGraalVMのサポートが入っていて、GraalVMを使ってMicronautアプリのNative Imageを生成することができる。

ドキュメントはこの辺: https://docs.micronaut.io/1.1.0/guide/index.html#graal

Micronautのバージョンは1.1.0。

## graal-native-image feature

Micronautのcreate-appをするときに、graal-native-imageをfeatureオプションにつける。

❯ mn create-app hello-graal --features graal-native-image
| Generating Java project...
| Application created at /home/bufferings/workspace/micronaut/hello-graal

と、プロジェクトの生成と同時に、GraalVMでNative Imageをビルドするときのためのファイルがいくつか生成される。

## Dockerでのビルドがいまいち?

その作成されるファイルの中にDockerfileとdocker-build.shがあって「docker-build.shを実行すればDockerのマルチステージビルドで、Native Imageがビルドされるよ!」ってドキュメントには書いてある。

んだけど、いまいちかなー。と思ったのでメモを書いとく。

## docker-build.sh

は、こうなってる

#!/bin/sh
docker build . -t hello-graal
echo
echo
echo "To run the docker container execute:"
echo "    $ docker run --network host hello-graal"

つまり、Dockerfileでビルドしてるよってだけね。じゃあDockerfileを見てみよう。

## Dockerfile

こうなってる。マルチステージビルドを使ってる。

FROM oracle/graalvm-ce:1.0.0-rc15 as graalvm
COPY . /home/app/hello-graal
WORKDIR /home/app/hello-graal
RUN native-image --no-server -cp build/libs/hello-graal-*.jar

FROM frolvlad/alpine-glibc
EXPOSE 8080
COPY --from=graalvm /home/app/hello-graal .
ENTRYPOINT ["./hello-graal"]

### まず前半の4行は

  1. graalvmのイメージを使って
  2. カレントディレクトリーを /home/app/hello-graal にコピーして
  3. そこをWORKDIRにして
  4. native-image コマンドを実行してNative Imageをビルドしてる

んー。これだと、GradleによるJARファイルのビルドを事前に実行しておかないといけない・・・よね。もしDockerだけで完結させたいならこうかな?

RUN ./gradlew assemble \
 && native-image --no-server -cp build/libs/hello-graal-*.jar

ただ、これだとビルドのたびにGradleをダウンロードしてきてビルドするから時間がかかるよね。

### 次に後半の4行は

この部分だけ

COPY --from=graalvm /home/app/hello-graal .

これ、前半でビルドしたイメージから、必要なファイルをコピーしてきてるんだけど、ソースコードまで要らんくない?しかもルートディレクトリーにガッと突っ込んでる。そうじゃなくて、

COPY --from=graalvm /home/app/hello-graal/hello-graal .

Native Imageだけ取ってこれたらいいかなと。あと、これは僕の好みだけなんだけど、ルート直下に置くよりは、なんかディレクトリーいっこ切っておきたいな。

FROM frolvlad/alpine-glibc
EXPOSE 8080
WORKDIR /app
COPY --from=graalvm /home/app/hello-graal/hello-graal .
ENTRYPOINT ["/app/hello-graal"]

### ということで

こうなった

FROM oracle/graalvm-ce:1.0.0-rc15 as graalvm
COPY . /home/app/hello-graal
WORKDIR /home/app/hello-graal
RUN ./gradlew assemble \
 && native-image --no-server -cp build/libs/hello-graal-*.jar

FROM frolvlad/alpine-glibc
EXPOSE 8080
WORKDIR /app
COPY --from=graalvm /home/app/hello-graal/hello-graal .
ENTRYPOINT ["/app/hello-graal"]

### 実行するには

host networkモードを使うんだ!ってドキュメントに書いてある。

$ docker run --network=host hello-world

僕のマシンはUbuntuだからいいんだけど、Docker for Macだと現時点ではサポートされてないよね。。。

https://docs.docker.com/network/host/

素直にpublishオプションを使っていいんじゃないかな。

$ docker run -p 8080:8080 hello-world

## そこまでしてDocker使わなくても・・・

いいかなと思う。さっきも書いたけどDocker使うとgradlewの実行遅いし。だから、素直にホストマシンにsdkmanでGraalVM使ってやるといいのかなと。思うのであった。

❯ sdk use java 1.0.0-rc-15-grl

Using java version 1.0.0-rc-15-grl in this shell.

❯ ./gradlew assemble
Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details

BUILD SUCCESSFUL in 11s
10 actionable tasks: 7 executed, 3 up-to-date
<-------------> 0% WAITING
> IDLE

❯ native-image --no-server -cp build/libs/hello-graal-*.jar
[hello-graal:31214]    classlist:   6,843.33 ms
[hello-graal:31214]        (cap):   1,346.60 ms
[hello-graal:31214]        setup:   3,216.68 ms
[hello-graal:31214]   (typeflow):  52,258.57 ms
[hello-graal:31214]    (objects):  38,803.47 ms
[hello-graal:31214]   (features):   2,844.33 ms
[hello-graal:31214]     analysis:  97,553.99 ms
[hello-graal:31214]     universe:   2,264.42 ms
[hello-graal:31214]      (parse):   7,780.03 ms
[hello-graal:31214]     (inline):  10,103.81 ms
[hello-graal:31214]    (compile):  85,401.85 ms
[hello-graal:31214]      compile: 106,246.90 ms
[hello-graal:31214]        image:   5,274.68 ms
[hello-graal:31214]        write:   1,565.77 ms
[hello-graal:31214]      [total]: 223,483.88 ms

❯ ./hello-graal
23:31:52.391 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 169ms. Server Running: http://localhost:8080
^C

❯ ./hello-graal
23:32:00.775 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 64ms. Server Running: http://localhost:8080

お。何も設定してないから速いってのもあるけど200msとか100msとかで起動するのは良いなー。

ということで、もうちょっとMicronautとGraalVMのNative Image周りを勉強してみようと思う。

MicronautでStaticコンテンツをハンドリング

今日使ってみたのは最新のバージョン1.1.0

❯ mn --version
| Micronaut Version: 1.1.0
| JVM Version: 12

## リファレンス

Staticコンテンツのハンドリングはドキュメントのこの部分

https://docs.micronaut.io/1.1.0/guide/index.html#staticResources

Javadocはこれだね

https://docs.micronaut.io/1.1.0/api/io/micronaut/web/router/resource/StaticResourceConfiguration.html

## 準備

プロジェクトを作って(spockは趣味)

❯ mn create-app hello-mn --features spock
| Generating Java project...
| Application created at /home/bufferings/workspace/micronaut/hello-mn

IDEAでImportして、Annotation Processorを有効化。

前回はIDEAからはうまく動かなかったけど、今回は普通に動いたや。

## こんな感じにした

f:id:bufferings:20190413120710p:plain:w600

application.yml に設定を入れて

micronaut:
  router:
    static-resources:
      default:
        paths:
          - "classpath:static"

classpath:staticにしたから、resourcesの下にstaticディレクトリーを作って、そこにsample.txtを置いた。

で、ブラウザで見てみたら見えた。

f:id:bufferings:20190413121048p:plain:w300

おわりー。

#DevOpsDaysTokyo で「サービス運用をまんなかにおいた開発」って発表をしましたー

楽しかった!

f:id:bufferings:20190410102555p:plain

DevOpsDaysTokyo

組織文化、Agile開発、自動化、ツール、とか、幅広くて、色んな話が聞こえてくるので面白いなぁ。

www.devopsdaystokyo.org

発表資料

はこちら。なんとなく英語で作ったけど、発表は日本語でやりました。

開発するときは、サービスを育てるってことを考えながら作ることが大切だと思う。ってお話でした。何人かに良かったよーって言ってもらえたので喜んだ。

togetter.com

今日は

スポンサーブースにいたりするので遊びに来てねー。

f:id:bufferings:20190410103621j:plain

エンジニアが何か問題にぶつかったときにあるといい力を5個

最近ちょこちょこ相談されることがあって、直接のスキルではないけど、こういうのもスキルだよなぁって思ったので、思いついた順に書いてみる。5個になった。

## 1. 問題を切り分ける力

「これがなぜか動かない」って相談されたときって、いくつかの要素が絡んでることが多い。

なので「ここは明らかに問題ないでしょう」という一番土台のところからチェックを始める。そうすると「え?そこは問題ないと思いますよ?」って言われるので「うん、それを『問題ないと思う』じゃなくて『問題ない』って断言できるようにしようと思って」みたいな会話をよくする。

可能性をひとつずつつぶしていくと「ここだなぁ」って場所が見つかって、そしたら、もうあとはそんなに難しくない。ひとつずつ確認していくのって遠回りに見えるけど、結局その方が確実ではやいと思う。

## 2. 想像と事実を切り分ける力

↑と絡んで、想像や思い込みなのに、「ここは問題ありません」って断言をする人もいる。ので「それは何かを確認してそう断言してるの?それとも想像?」って聞くと「( ゚д゚)ハッ!・・・確認はしてません」ってなる。「じゃ、そこ確認してみよう」。

自分の中の想像と事実を切り分ける力もそうだけど、誰かの中の想像と事実を切り分ける力も大切だね。

## 3. 探す力

問題にぶつかって、調べてみるかーってなるとして、ネットを探し回るのもスキルがいるよなぁって思う。だいたい英語のほうが情報が多い。

エラーメッセージの中からキーワードを選んだり。期間を指定したり、サイトを指定したり。Stack Overflowを見に行ったり。GithubのIssueを探してみたり。あと、うちの部署は全員Safaribooksを読めるので、そこで探したりもするなぁ。

こういうサイトにはたぶん載ってないとか。この翻訳は原文をあたったほうが良さそうとか。この人の記事なら信頼できるとか。もある。

それと、社内のドキュメントを探す力も重要。「ここにあったよ」って伝えると「え?そんなの知らなかった!」って言われることが多いんだけど「僕も知らなかったけど、探したら見つかったよ。ちょっとコツがいるよね」って感じ。このスキルは外では身につかないのが難しいところ。

## 4. 公式ドキュメントを読む力

公式ドキュメントに書いてある場合も多い。例えばSpringBootの設定を外部からどう与えるかとか。なので目を通しておいて、必要なときに「たしかあそこに書いてたなー」ってチェックできるようにしておく。読んでなくて想像で動かしてる人も多そう。

## 5. ソースコードを読む力

目の前にソースコードがあるのに「このスクリプトが動かないんですよね・・・」って言われるときも多い。「ソースコード読んでみたんだけど、こうやれば良さそうよ?」みたいな。「じゃあ、この場合はどんな風に動くんですか?」って聞かれて(ほほー。自分で読むってのは想像もしないのかー)って思いつつ「そうね。一緒にこのソースコード読もうか」ってなる。

Githubソースコードが公開されてるプロダクトなら見に行ったらいい。Vagrantの動きが気になったときとか、k8sのReadiness Probeが気になったときに、見に行ってほほーってなったりしてた。RubyとかGoとか分からなくても雰囲気で読む力がついてきたのかな。ちょっと前のバージョンを使ってる場合とかだと、ブランチの中を探してみたりもする。

コンテナの中身がどうなってるか分からない!って言う人もいるけど「それ、GithubにDockerfileあるからそれ読んだらだいたい分かりますよー」って話もしたりする。

## そんな感じ

エラーメッセージを読む力、はもういいよね。

とはいえ、書きながら自分自身も、Terraformの公式ドキュメントに目を通してないなぁとか思った。もっと頑張ろっと。

コードの読みやすさ

処理が一行ずつ書いてあるより、意味のある塊に閉じ込めてくれてると、読みやすいなぁって思う。僕は、たぶん、こんな順番で書いてる。

## 1. ゴールまで行く

処理を一行ずつ書いていって、さくっと動くことを確認する。

  • ちなみに、このときの僕→(おれすごい。とぎすまされてる!)
  • そして、このままにしてた場合の1週間後の僕→(なんもわからん。先週のおれ、叩く)

なので、ここで終わりにはしない。

## 2. グルーピングする

その中から意味のある塊を見つけて、グルーピングしてみる。

その意味のある塊に名前をつける。

あーでもないこーでもないって場所を動かしてみて、しっくりくるところを探す。

## 3. コードを眺める

さっきまでは書く頭だったけど、今度は頭を切り替えて、読む頭でコードを眺める。

ぱっと見て意味の分かりやすいコードになっているかを考える。

一晩寝かせたあとだとより良い。

そのファイルはそれで終わり。自分の参照用としては使うけど、実際のコードとしては使わない。

## 4. 実装する

最初に、そのグルーピングした意味の塊に対するテストを書く。

それから、実装を書く。のを繰り返して仕上げていく。

それをやってる中で気づいたり、思いついたりすることも結構あるかなぁ。

## 5. コードを眺める

最後にもういちど、読む頭で眺める

既存のコードとかアーキテクチャーにもよるけど、だいたいそんな感じ。