Kubernetes利用時のJavaアプリケーションリソースの制限に関するよくある誤解

このシリーズの最初の記事では、**Kubernetes**を使用する際のJavaアプリケーションリソースの制限について、よくある誤解のいくつかを見ていきます。

*本ブログは英語版からの翻訳です。オリジナルは[こちら](https://www.alibabacloud.com/blog/kubernetes-demystified-restrictions-on-java-application-resources_594108)からご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。*

このシリーズの記事では、企業のお客様が[Kubernetes](https://www.alibabacloud.com/ja/product/kubernetes)を使用する際に遭遇する一般的な問題のいくつかを探ります。

[コンテナ技術](https://www.alibabacloud.com/ja/product/container-service)がますます洗練されるにつれ、アプリケーションプラットフォームの基盤としてDockerとKubernetesを選択する企業顧客が増えています。しかし、これらの顧客は実際には多くの問題に遭遇しています。このシリーズの記事では、顧客がこのプロセスをナビゲートするのを支援してきたアリババクラウドのコンテナサービスチームの経験から導き出されたいくつかの洞察とベストプラクティスを紹介します。

Javaアプリケーションのコンテナ化されたデプロイメントについては、コンテナリソースの制限を設定しているにもかかわらず、アクティブなJavaアプリケーションコンテナがOOM Killerによって不可解にも強制終了されてしまうという報告があります。

この問題は、コンテナリソース制限とそれに対応するJVMヒープサイズを正しく設定していないという、非常に一般的なミスの結果です。

ここでは、Tomcat アプリケーションを例に挙げます。そのインスタンスコードと Kubernetes デプロイメントファイルは [GitHub](https://github.com/denverdino/system-info?spm=a2c65.11461447.0.0.19b432f1QUkwgh) から入手できます。

```
git clone https://github.com/denverdino/system-info
cd system-info`
```

以下のKubernetesポッドの定義を使用しています。

1、ポッド内のアプリは初期化コンテナであり、1つのJSPアプリケーションを`Tomcat`コンテナの "webapps "ディレクトリにコピーすることを担当します。注：イメージでは、JSPアプリケーションのindex.jspは、JVMとシステムリソース情報を表示するために使用されています。
2、`Tomcat`コンテナはアクティブなままで、最大メモリ使用量を256MBに制限しています。

```
apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  initContainers:
  - image: registry.cn-hangzhou.aliyuncs.com/denverdino/system-info
    name: app
    imagePullPolicy: IfNotPresent
    command:
      - "cp"
      - "-r"
      - "/system-info"
      - "/app"
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: tomcat:9-jre8
    name: tomcat
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - mountPath: /usr/local/tomcat/webapps
      name: app-volume
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "256Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: "500m"
  volumes:
  - name: app-volume
    emptyDir: {}
```

以下のコマンドを実行して、アプリケーションのデプロイとテストを行います。

```
$ kubectl create -f test.yaml
pod "test" created
$ kubectl get pods test
NAME      READY     STATUS    RESTARTS   AGE
test      1/1       Running   0          28s
$ kubectl exec test curl http://localhost:8080/system-info/
...
```

これで、システムのCPUやメモリなどの情報がHTML形式で表示されるようになりました。html2textコマンドを使用して、情報をテキスト形式に変換することができます。

注: ここでは、2C 4G ノードでアプリケーションをテストしています。異なる環境でのテストにより、わずかに異なる結果が得られる可能性があります。

```
$ kubectl exec test curl http://localhost:8080/system-info/ | html2text

Java version     Oracle Corporation 1.8.0_162
Operating system Linux 4.9.64
Server           Apache Tomcat/9.0.6
Memory           Used 29 of 57 MB, Max 878 MB
Physica Memory   3951 MB
CPU Cores        2
                                          **** Memory MXBean ****
Heap Memory Usage     init = 65011712(63488K) used = 19873704(19407K) committed
                      = 65536000(64000K) max = 921174016(899584K)
Non-Heap Memory Usage init = 2555904(2496K) used = 32944912(32172K) committed =
                      33882112(33088K) max = -1(-1K)
```

見ての通り、コンテナ内のシステムメモリは3,951MBですが、JVMの最大ヒープサイズは878MBです。なぜこのようなことになっているのでしょうか？コンテナのリソース容量を256MBに設定していなかったのでしょうか？この状況では、アプリケーションのメモリ使用量は256MBを超えていますが、JVMはガベージコレクション(GC)を実装していません。むしろ、システムのOOMキラーによってJVMプロセスが直接殺されています。

問題の根本的な原因：

1、JVMのヒープサイズを設定しないと、ホスト環境のメモリサイズに基づいてデフォルトで最大ヒープサイズが設定されてしまいます。
2、Dockerコンテナはプロセスが使用するリソースを制限するためにcgroupsを使用します。そのため、コンテナ内のJVMがまだホスト環境のメモリとCPUコアに基づいたデフォルトの設定を使用している場合、これは正しくないJVMヒープ計算になります。


同様に、デフォルトのJVM GCおよびJITコンパイラのスレッド数は、ホストCPUコア数によって決定されます。1つのノードで複数のJavaアプリケーションを実行する場合、CPU制限を設定しても、GCスレッドがアプリケーション間の切り替えを先取りしてしまい、アプリケーションのパフォーマンスに影響を与える可能性があります。

この問題の根本的な原因がわかったので、解決は簡単です。

#ソリューション
###cグループのリソース認識を有効にする
Javaコミュニティもこの問題を認識しており、現在ではJava SE 8u131+とJDK 9でコンテナリソース制限の自動検知をサポートしています: [https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory-limits](https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory-limits?spm=a2c65.11461447.0.0.19b432f1RHoeWq)

このメソッドを使用するには、以下のパラメータを追加します。

```
java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap ⋯
```

先ほどの`Tomcat`コンテナの例に引き続き、環境変数「JAVA_OPTS」を追加します。

```
apiVersion: v1
kind: Pod
metadata:
  name: cgrouptest
spec:
  initContainers:
  - image: registry.cn-hangzhou.aliyuncs.com/denverdino/system-info
    name: app
    imagePullPolicy: IfNotPresent
    command:
      - "cp"
      - "-r"
      - "/system-info"
      - "/app"
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: tomcat:9-jre8
    name: tomcat
    imagePullPolicy: IfNotPresent
    env:
    - name: JAVA_OPTS
      value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
    volumeMounts:
    - mountPath: /usr/local/tomcat/webapps
      name: app-volume
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "256Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: "500m"
  volumes:
  - name: app-volume
    emptyDir: {}
```

さて、新しいポッドを展開してテストを繰り返します。

```
$ kubectl create -f cgroup_test.yaml
pod "cgrouptest" created

$ kubectl exec cgrouptest curl http://localhost:8080/system-info/ | html2txt
Java version     Oracle Corporation 1.8.0_162
Operating system Linux 4.9.64
Server           Apache Tomcat/9.0.6
Memory           Used 23 of 44 MB, Max 112 MB
Physica Memory   3951 MB
CPU Cores        2
                                          **** Memory MXBean ****
Heap Memory Usage     init = 8388608(8192K) used = 25280928(24688K) committed =
                      46661632(45568K) max = 117440512(114688K)
Non-Heap Memory Usage init = 2555904(2496K) used = 31970840(31221K) committed =
                      32768000(32000K) max = -1(-1K)
```

見ての通り、最大JVMヒープサイズは112MBに変更されており、アプリケーションがOOMキラーによって殺されることはありません。しかし、これは別の問題を提起しています。コンテナメモリの最大制限を256MBに設定しているのに、なぜ最大JVMヒープメモリを112MBにしか設定しないのでしょうか？

その答えは、JVMメモリ管理の細かい点に関係しています。JVMにおけるメモリ消費には、ヒープメモリと非ヒープメモリの両方が含まれます。クラスメタデータやJIT準拠コード、スレッドスタック、GCなどに必要なメモリは、非ヒープメモリから取り出されます。そのため、cgroupのリソース制限に基づいて、JVMは、システムの安定性を確保するために、メモリの一部を非ヒープ用に予約します。(先ほどの例では、Tomcatを起動した後、非ヒープメモリが32MB近くを占有していることがわかります)。

最新バージョンのJDK 10では、コンテナ内のJVM操作にさらなる最適化と機能強化が行われました。

###コンテナ内のcgroupリソース制限の認識
JDK 8と9の新機能が使えない場合（例えば、古いJDK 6のアプリケーションをまだ使っている場合）、コンテナ内のスクリプトを使ってコンテナのcgroupのリソース制限を取得し、これを使ってJVMのヒープサイズを設定することができます。

Docker 1.7からはコンテナのcgroup情報がコンテナ内にマウントされ、アプリケーションは`/sys/fs/cgroup/memory/memory.limit_in_bytes`などのファイルからメモリやCPUなどの設定を取得できるようになりました。そのため、コンテナ内のアプリケーションの起動コマンドには、-Xmx、-XX:ParallelGCThreadsなど、cgroupの設定に基づいた正しいリソース設定が含まれています。

###結論
この記事では、コンテナでJavaアプリケーションを実行する際に発生する一般的なヒープ設定の問題について見ていきます。コンテナは仮想マシンとは異なり、リソースの制限がcグループを使って実装されています。さらに、内部のコンテナプロセスがcgroupの制限を認識していない場合、メモリやCPUの割り当てによってリソースの競合や問題が発生する可能性があります。

この問題は、新しいJVM機能やカスタムスクリプトを使用してリソース制限を正しく設定することで、非常に簡単に解決することができます。これらのソリューションは、リソース制限の問題の大部分に対処します。

しかし、これらのソリューションは、コンテナアプリケーションに影響を与える1つのリソース制限問題を未解決のままにしています。古い監視ツールや「free」や「top」などのシステムコマンドの中には、コンテナ内で実行する際にホストのCPUやメモリの設定をまだ取得しているものがあります。これは、特定の監視ツールがコンテナ内で実行されているときにリソース消費量を正確に計算できないことを意味します。コミュニティで提案されているこの問題に対する共通の解決策は、コンテナのリソース可視化動作と仮想マシンとの間の一貫性を維持するために[LXCFS](https://github.com/lxc/lxcfs?spm=a2c65.11461447.0.0.19b432f1eicljw)を使用することです。後続の記事では、Kubernetes上でのこの方法の使用について説明します。

[Alibaba Cloud Kubernetes Service](https://www.alibabacloud.com/ja/product/kubernetes)は、Kubernetesの整合性が認証された最初のサービスです。Kubernetesクラスタのライフサイクル管理を簡素化し、Alibaba Cloud製品への組み込み統合を提供します。さらに、同サービスはKubernetesの開発者エクスペリエンスをさらに最適化し、ユーザーはクラウドアプリケーションの価値とさらなるイノベーションに集中できるようになります。

*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/ja)*