Java
docker
Java10

Java10のDocker対応について

数字的には節目となるJava10が公開され、Java界隈は久々の春を満喫しつつ、これから始まるアップデートレースに戦々恐々としていると思います。

Java10の新規フィーチャーはいろいろなブログで紹介されていますが、個人的に気になっていたDocker対応について、少し調べてみました。

Java10のリリースノート :  http://www.oracle.com/technetwork/java/javase/10-relnote-issues-4108729.html

Dockerについては3つほど対応が書いてありますが、気になるのがこちらです。

Improve docker container detection and resource configuration usage
The following changes have been introduced in JDK 10 to improve the execution and configurability of Java running in Docker containers:

JDK-8146115 Improve docker container detection and resource configuration usage
The JVM has been modified to be aware that it is running in a Docker container and will extract container specific configuration information instead of querying the operating system. The information being extracted is the number of CPUs and total memory that have been allocated to the container. The total number of CPUs available to the Java process is calculated from any specified cpu sets, cpu shares or cpu quotas. This support is only available on Linux-based platforms. This new support is enabled by default and can be disabled in the command line with the JVM option:

-XX:-UseContainerSupport

In addition, this change adds a JVM option that provides the ability to specify the number of CPUs that the JVM will use:

-XX:ActiveProcessorCount=count

This count overrides any other automatic CPU detection logic in the JVM.

 
Doockerコンテナの中でJVMが動作する場合、従来はDockerコンテナの設定を見ずにOS設定を見ていたけどもJava10でそれが改善されたよ、というものでした。
ということで、実際にどんな感じなのか、取り急ぎJava10込みのDockerイメージを作成して試してみました。

Dockerイメージはこのスクリプトで作成しました。 https://gist.github.com/c9katayama/634b14ea65f3910448e0c93b4637a1c1
上記スクリプトをダウンロードして、dockerhubのユーザー名を適切に設定した後、OracleJDKのLinux版tar.gzを同フォルダにおいて実行することで、UbuntuのDockerイメージが作成できます。

CPU数のレポート

比較対象として、Java8(162) と Java9(9.0.4)のイメージも作成しました。実行環境はAmazonLinux 2017.09、EC2インスタンスはm4.xlarge( 4CPU/mem 16G )です。

Java8

以下のスクリプトを実行しました。

//dockerコンテナ起動
docker run -it c9katayama/java8:162

//javaコード実装
echo 'public class T{ public static void main(String[] args){ System.out.println("CPU:"+Runtime.getRuntime().availableProcessors()); }}' > T.java

//コンパイルと実行
javac -cp . T.java && java -cp . T

//結果
CPU:4

Java8 でDockerコンテナにプロセッサ数指定なしの場合、インスタンス数と同一の4が取得できました。
つぎにDockerコンテナ起動時にCPU数を1にしてみます(正確には2番目のプロセッサを割りあて)

//dockerコンテナ起動
docker run --cpuset-cpus 1 -it c9katayama/java8:162

//javaコード実装と実行(省略)

//結果
CPU:1

予想に反して、Dockerコンテナに割り当てた正しいCPU数が取得できました。

Java9

次にJava9です。Java9からJShellが使えるので、こちらを使います。

docker run -it c9katayama/java9:9.0.4
jshell
jshell> System.out.println("CPU:"+Runtime.getRuntime().availableProcessors())
CPU:4

これは問題なくCPU数4が取れています。次にCPUを割り当ててみます

docker run --cpuset-cpus 1 -it c9katayama/java9:9.0.4
jshell
jshell> System.out.println("CPU:"+Runtime.getRuntime().availableProcessors())
CPU:1

これも予想に反して、正しいCPU数が取れました。

Java10

本命のJava10ですが、これまでの流れ通り、

docker run -it c9katayama/java10:10
->CPU:4
docker run  --cpuset-cpus 1 -it c9katayama/java10:10
->CPU:1

でした。
ということで、検証した限りでは--cpuset-cpusオプションはJava8から動作しているようですので、

int threadNum = Runtime.getRuntime().availableProcessors() /2;

のようなコードも挙動が変わるようなことはなさそうです。

cpusオプション(追記)

Docker1.13から、 -cpus というオプションがあるようで、こちらについても調べてみました。

Java8

docker run --cpus 1.0 -it c9katayama/java8:162
//省略
CPU:4

Java9

docker run --cpus 1.0 -it c9katayama/java9:9.0.4
//省略
CPU:4

Java10

docker run --cpus 1.0 -it c9katayama/java10:10
//省略
CPU:1

--cpusオプションについては、Java9までは正しく対応ができていないようで、Java10ではDockerコンテナに割り当てたものが正しく取れるようになっていました。

メモリ量のレポート

次にメモリ量のほうを確認してみます。使うメソッドは

Runtime.getRuntime().maxMemory();

です。分かりやすいように、メモリ量指定あり/なしでまとめます。

メモリ量指定なし

Java8

docker run -it c9katayama/java8:162
echo 'public class T{ public static void main(String[] args){ System.out.println("MEM:"+Runtime.getRuntime().maxMemory()/1024/1024+"M"); }}' > T.java
javac -cp . T.java && java -cp . T
MEM:3568M

Java9

docker run -it c9katayama/java9:9.0.4
jshell> System.out.println("MEM:"+Runtime.getRuntime().maxMemory()/1024/1024+"M")
MEM:4014M

Java10

docker run -it c9katayama/java10:10
jshell> System.out.println("MEM:"+Runtime.getRuntime().maxMemory()/1024/1024+"M")
MEM:4014M

Java8,9,10とも、OSのメモリ量を元にデフォルト値(OSメモリ量の1/4)が設定されている模様で、これは期待通りです(若干Java8が少ないのが気になりますが)

メモリ量指定あり

次にメモリ指定(512M)を指定してみます。

Java8

docker run -m 512m -it c9katayama/java8:162
echo 'public class T{ public static void main(String[] args){ System.out.println("MEM:"+Runtime.getRuntime().maxMemory()/1024/1024+"M"); }}' > T.java
javac -cp . T.java && java -cp . T
MEM:3568M

Java9

docker run -m 512m -it c9katayama/java9:9.0.4
jshell> System.out.println("MEM:"+Runtime.getRuntime().maxMemory()/1024/1024+"M")
MEM:4014M

Java10

docker run -m 512m -it c9katayama/java10:10
jshell> System.out.println("MEM:"+Runtime.getRuntime().maxMemory()/1024/1024+"M")
MEM:123M //減ってる

Java8とJava9は、華麗にDockerコンテナの設定をスルーしていますが、Java10はDockerコンテナの指定を元に、最大メモリ量を設定しているようです。

影響について

検証した範囲では、CPUについては(少なくとも--cpuset-cpusオプションについては)問題なさそうですが、--cpusオプションは挙動が変わっているので見直したほうが良さそうです。特にparalellStream()を利用しているような場合、プロセッサ数によって利用するスレッド数が変わるため、パフォーマンスに影響があるかもしれません。

メモリについて、 1.Dockerコンテナにメモリ量を指定していた 2.JavaVMにメモリオプションなし のような状況で今まで上手く動いていたようなケースだと、もしかすると問題があるかもしれないです。

なおこの挙動をなくすJVMオプション( -XX:-UseContainerSupport )があるというのを @sugarlifeさん( https://twitter.com/sugarlife )に教えてもらいました。
https://twitter.com/sugarlife/status/976355508343881729

実際にJVMに渡すと、Java9以前の挙動に戻りました。

docker run -m 512m -it c9katayama/java10:10
echo 'public class T{ public static void main(String[] args){ System.out.println("MEM:"+Runtime.getRuntime().maxMemory()/1024/1024+"M"); }}' > T.java
javac -cp . T.java 
java -XX:-UseContainerSupport -cp . T  //オプション付きで実行
MEM:4014M

いずれにせよ、メモリ周りはJava10のときに一度見直してもよいかと思います。

Dockerのリソース割当 : https://docs.docker.com/config/containers/resource_constraints/#configure-the-default-cfs-scheduler