Java10のDocker対応について

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

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

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

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

```
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
以下のスクリプトを実行しました。

```bash
//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番目のプロセッサを割りあて）

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

//javaコード実装と実行（省略）

//結果
CPU:1
```

予想に反して、Dockerコンテナに割り当てた正しいCPU数が取得できました。
### Java9
次にJava9です。Java9からJShellが使えるので、こちらを使います。

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

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

```bash
docker run --cpuset-cpus 1 -it c9katayama/java9:9.0.4
jshell
jshell> System.out.println("CPU:"+Runtime.getRuntime().availableProcessors())
CPU:1
```
これも予想に反して、正しいCPU数が取れました。

### Java10
本命のJava１０ですが、これまでの流れ通り、

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

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

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

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

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

### Java8
```bash
docker run --cpus 1.0 -it c9katayama/java8:162
//省略
CPU:4
```

### Java9
```bash
docker run --cpus 1.0 -it c9katayama/java9:9.0.4
//省略
CPU:4
```

### Java10
```bash
docker run --cpus 1.0 -it c9katayama/java10:10
//省略
CPU:1
```

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


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

```java
Runtime.getRuntime().maxMemory();
```

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

### メモリ量指定なし

#### Java8
```bash
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
```bash
docker run -it c9katayama/java9:9.0.4
jshell> System.out.println("MEM:"+Runtime.getRuntime().maxMemory()/1024/1024+"M")
MEM:4014M
```
#### Java10
```bash
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
```bash
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
```bash
docker run -m 512m -it c9katayama/java9:9.0.4
jshell> System.out.println("MEM:"+Runtime.getRuntime().maxMemory()/1024/1024+"M")
MEM:4014M
```
#### Java10
```bash
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以前の挙動に戻りました。

```bash
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