DockerとJava
今年のCode Oneでは、コンテナ技術であるDockerに関するセッションは40件もあり、コンテナ技術に関する関心が高まっていることが伺えます。コンテナはLinuxカーネルの機能などを利用してOSの各種リソースを分離することで、他プロセスなどから独立/分離したアプリケーション実行環境を構築する技術で、特にDockerが有名です。ここでは、JavaをDockerコンテナで動かす際のノウハウに関するセッションを紹介します。
Javaのコンテナ対応はJava 8から取り組まれていますが、Java 10になってコンテナ対応機能が拡充しました。Java 10よりも前のバージョンでは不具合があり注意が必要です。この不具合についてはAndreas Hochleitner氏のセッション(JVM CPU and Memory Settings for Docker Containers in Production [DEV6060])の内容を紹介します。
Java 8(u121以前)では、JVM(Java Virtual Machine)がコンテナ環境に割り振られたCPUやメモリなどのリソース設定を無視し、コンテナのホスト環境のリソース情報を読み込んでしまう不具合があります。Javaは実行環境のCPUやメモリなどのリソース情報をもとにガベージコレクタやJVMに関する各種パラメータを自動で設定します。コンテナで動作するJavaがホスト環境のリソース情報をもとにパラメータ設定を行ってしまうことで、コンテナに割り振られている以上のCPUやメモリなどのリソースを使用しクラッシュしてしまうことがあります。この不具合は改善されてきたものの、Java 9以前ではコンテナ対応自体が試験的な導入に留まっています。Java 10でコンテナ対応機能が拡充し、コンテナのリソース割り振りに則した各種パラメータを設定できるようになりました。そのため、DockerでJavaを動作させる場合、Java 10以降の新しいバージョンのJDK(Java Development Kit)の使用が推奨されています。
一般にコンテナイメージのサイズはより小さくなることが望ましいとされています。Java実行環境を含むコンテナイメージ作成については、OracleのMikael Vidstedt氏とBob Vandette氏のセッションの内容(Java in a World of Containers [DEV6100])を紹介します。
このセッションでは、コンテナイメージを小さくするための3点のテクニックについて紹介されています。
- Java実行環境のサイズ削減
- クラスメタデータの共有化
- Dockerリソース制限
Java実行環境のサイズ削減(Java 9以降)
Java開発環境(JDK: Java Development Kit)11のサイズは300MBほどあり、そのままで使用するにはサイズが大きすぎます。この中にはアプリケーションが使用しないモジュールが含まれています。そこで、これらの不必要なモジュールを取り除き、アプリケーションに必要なモジュールだけを残したサイズの小さなJava実行環境(JRE: Java Runtime Environment)を作成します。このサイズの小さなJREを作成するためには、JDK 9で追加されたjlinkを使用します。jlinkは指定したJavaモジュールのみをまとめたカスタムJREを作成するツールです。
図1はカスタムJREを作成し、java -versionコマンドを実行するDockerfileの例です。この例では、空行で区切られた前半部分でカスタムJREを作成し、後半部分でカスタムJREを使用してjava -versionコマンドを実行します。特に注目すべきは5行目の下線を引いた箇所です。jlinkコマンドを使用してJavaの最小セットであるjava.baseモジュールを指定したカスタムJRE(my-jre)を作成しています。例のようにjava.baseのみであれば50MB程度まで小さくすることができます。
FROM oraclelinux:latest AS java-build ADD openjdk-11+28_linux-x64_bin.tar.gz /opt/jdk ENV PATH=/opt/jdk/jdk-11/bin:$PATH WORKDIR /jlink RUN jlink --output my-jre --add-module java.base FROM oralcelinux:latest COPY --from=java-build /jlink/my-jre /opt/my-jre ENV PATH=/opt/my-jre/bin:$PATH CMD ["java", "-version"]
クラスメタデータの共有化(Java 10以降)
一般にコンテナを使用する場合、同じ処理を行う複数のコンテナを並列で同時に動作させるケースが考えられます。その場合、JVMごとに同じクラスメタデータを重複してロードすることになります。Application Class Data Sharing(App CDS)を使用することで、この無駄を解消できます。App CDSは図2の様に、JavaクラスメタデータをJVM間で共有する機能です。事前にクラスメタデータを含むアーカイブファイルを作成し、JVM起動時にメモリマッピングして読み込ませることができます。一からクラスをロードするよりも早くロードすることができ、起動時間短縮にもつながります。
Dockerリソース制限(Java 10以降)
コンテナごとに必要な量だけのCPUとメモリリソースを割り当てることも重要です。前述したようにJava 10からであればDockerのリソース制限設定がそのままJavaにも適用されます。DockerのCPUオプションはJVMのワーカスレッド数に、メモリオプションはJavaヒープなどの最大適用可能サイズを制限します。
セッションでは他にも試験的な機能として、Project Portola(Alpine Linuxとlibc実装のmuslを使用した軽量イメージ)、Ahead-of-Time Compilation(事前コンパイルされたコードのコンテナ間共有)、 SubstrateVM(GraalVM上で動作する高速起動、低イメージサイズ、低メモリ消費のネイティブイメージの作成)が紹介されていました。
JavaのDockerコンテナ対応はJava 10で実用の段階を迎え、マイクロサービスやサーバレス技術とも相まって今後も目が離せない状況が続きそうです。
