Javaバージョン毎におけるkubernetes (GKE)上でのメモリ、CPU認識のデフォルト動作の違い

# はじめに

Java 8においても、
 [Oracle Blogs 日本語のまとめ - [Java] Java SE support for Docker CPU and memory limits] (https://orablogs-jp.blogspot.com/2017/05/java-se-support-for-docker-cpu-and.html)
の記事から、8u131以降はある程度 container.resource.limit.cpu、container.resource.limit.memoryを見て上手い事やってくれるのかな？
と思いきや、いまいちな挙動だったので、kubernetesで動かした際のjava8、java10、java11(ea)でのCPUの認識、及びメモリサイズの確保部分について調べてみました。

特に、GCの並列実行(スレッド数)や、各種並列スレッドのプール数等にも影響がある、 Runtime#availableProcessorsの動きがバージョンで変わるのか？という部分が
https://bugs.openjdk.java.net/browse/JDK-8140793
を見ても、2018/7/31時点でまだfixedVersionが出ていないのでどのバージョンでもまともに動かないのかどうかが気になった次第です。

# 結論
 - kubernetesでJavaアプリケーションを動作させる際、CPU、メモリを正しく認識させたいならjava10以降ならちゃんと認識してくれる。
 - java8でもメモリはXmx、Xmsなどで制御可能。
 - java8ではRuntime#availableProcessorsは正しく制限かける事はできないが、GCThread等は制御可能。


# 前提Javaアプリケーション
適当なSpring Boot アプリケーションを動かしてそのmetrics情報と、`java -XX:+PrintGCDetails -XX:+PrintFlagsFinal -XX:+UnlockExperimentalVMOptions` をcontainer上で動かした際の情報を確認します。

Spring Bootのアプリケーションは、
https://github.com/h-r-k-matsumoto/spring-boot-sample
をベースとして、それぞれ下記のように変更しています。また、cointainerに対するリソース割当は、下記のようになります。

```020_deployments.yml
        resources:
          requests:
            cpu: 150m
            memory: 512Mi
          limits:
            cpu: 900m
            memory: 512Mi
```
deploymentsの全文は、
https://github.com/h-r-k-matsumoto/spring-boot-sample/blob/master/kubernetes/020_deployments.yml
です。

## java8 (no cgroup option)
pom.xmlのfrom imageを下記に変更します。特にJavaオプションでは何も制御しません。デフォルトのままです。

```pom.xml
  <image>openjdk:8u171-jre-alpine</image>
```

# java8 
pom.xmlのfrom imageを下記に変更します。

```pom.xml
  <image>openjdk:8u171-jre-alpine</image>
```

更に、下記jvmFlagオプションを追加します。ポイントは、 `UseCGroupMemoryLimitForHeap` です。
後は・・・私がいつもdocker 環境で動かす際につけているものです。

```pom.xml
  <jvmFlag>-XX:+UnlockExperimentalVMOptions</jvmFlag>
  <jvmFlag>-XX:+UseCGroupMemoryLimitForHeap</jvmFlag>
  <jvmFlag>-XX:ParallelGCThreads=1</jvmFlag>
  <jvmFlag>-XX:CICompilerCount=2</jvmFlag>
  <jvmFlag>-Djava.util.concurrent.ForkJoinPool.common.parallelism=1</jvmFlag>
```

# java10
pom.xmlのfrom imageを下記に変更します。オプションは指定無しです。

```pom.xml
  <image>openjdk:10-jre-slim</image>
```

# java11 (ea)
pom.xmlのfrom imageを下記に変更します。オプションは指定無しです。

```pom.xml
  <image>openjdk:11-jre-slim</image>
```

# 計測内容
[Spring Boot Actuatorのmetrics] (https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-metrics.html) 機能と、

```cmd:PrintFlagsFinal
 kubectl exec {pod-name} -- java -XX:+PrintGCDetails -XX:+PrintFlagsFinal -XX:+UnlockExperimentalVMOptions
```

を実行して取得した情報を利用します。


## Spring Boot Actuator - metrics

### system.cpu.count
http://pod-ip:port/actuator/metrics/system.cpu.count
で取得します。`Runtime#availableProcessors` の実行結果です。
ソースは[micrometer - ProcessorMetrics.java](https://github.com/micrometer-metrics/micrometer/blob/35890bdc64614c24a8117099b0dcdaa003eab798/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/ProcessorMetrics.java#L83)。

### jvm.memory.max (heap)
http://pod-ip-port/actuator/metrics/jvm.memory.max?tag=area:heap
で取得します。ヒープ領域の最大サイズです。
ソースは、[micrometer - JvmMemoryMetrics.java](https://github.com/micrometer-metrics/micrometer/blob/cff2e23445812852b5fa97b414958dbe595f0ae2/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java#L77-L96)。 `MemoryUsage#getMax` の結果です。

## PrintFlagsFinal結果

### MaxHeapSize
最大ヒープ管理領域のサイズです。[jvm.memory.max](#jvm.memory.max?tag=area:heap)と同じになる・・・はず。

### UseParallelGC
GCを並列実行するかどうかです。
参考：https://docs.oracle.com/javase/jp/8/docs/technotes/guides/vm/gctuning/collectors.html

### ParallelGCThreads
パラレルGCの際のスレッド数です。

# 計測結果

実行するノードのspecは、vCPUx2、メモリ7.5GBです。

|java version|system.cpu.count|jvm.memory.max(heap):MiB|MaxHeapSize:MiB|UseParallelGC|ParallelGCThreads|
|:-----------------------|-----:|----------:|-----------:|:------:|--:|
|java 11(ea)             |1     |123.75     |128.00      |false   |0|
|java 10                 |1     |123.75     |128.00      |false   |0|
|java 8                  |**2** |120.00     |128.00      |**true**|**2**|
|java 8 (no cgroup option)|**2** |**1,857.00**|**1,870.00**|**true**|2|

※java 8でparallelになってしまっているのは、-XX:+UseSerialGC のオプションでserialに変更可能。というか -XX:ParallelGCThreads=1 が効いていない…。

java11、java10は、特にオプション指定をしなかった場合は、containersに割り当てられたメモリサイズの `512Mi` から正しく計算され、MaxRAMFraction=4となるため、512/4 = 128 となっていました。
また、CPUに関しても、900mとしましたが、1コア分として判定されています。
java11、java10は、XmxやらXmsは指定せずとも、container上で正しく動きそうです。

java8の場合は、メモリに関しては制御可能です。ただし、CPU数に関しては上手い事手出しができないっぽいので… 　`Runtime#availableProcessors` から計算していてリソース確保している部分は、都度システムプロパティ等で設定した方がよさそうです。

jvm.memory.max(heap)と、MaxHeapSizeで多少の誤差が出てるのは・・・気にしないです！
もし「このようなオプションを指定した方が良い」、「ここ、間違ってるよ」とかあればご指摘お願いします。

# 参考

- FookJoinPoolとプール数の仕様： https://docs.oracle.com/javase/jp/8/docs/api/java/util/concurrent/ForkJoinPool.html#ForkJoinPool--
- javaのdocker環境サポートの話：https://orablogs-jp.blogspot.com/2017/05/java-se-support-for-docker-cpu-and.html