JavaのコンテナイメージにAlpineを使ったらどん詰まった話

# Javaを動かすコンテナイメージ

色々ありますよね。
まずOpenJDKがいくつかあって、それぞれが各ディストリビューションのJDKだったりJREだったりを出してます。

* [OpenJDK](https://hub.docker.com/_/openjdk) (今見たらDeprecatedなんですね…)
* [Amazon Corretto](https://hub.docker.com/_/amazoncorretto)
* [Eclipse Temurin](https://hub.docker.com/_/eclipse-temurin)

など

あとはGoogleが出してるDistrolessもJava用のイメージがありますね。
* [Distroless/Java](https://github.com/GoogleContainerTools/distroless/tree/main/java)

個人的にはEclipse Temurinがなんとなく好きで、Alpine Linuxが軽量なのは知っていたので、AlpineベースのJREイメージを安易に利用していました。

# 問題発生

以下の構成でJavaアプリケーションを作っていました。
| Language | Framework         | Docker Image                 |
|:--------:|:-----------------:|:----------------------------:|
| Java 17  | Spring Boot 3.2.0 | eclipse-temrin:17-jre-alpine |

アプリケーションとしては、
* Amazon S3からファイルをInputStreamでDLしてくる
* フォーマットを変換してLocalにファイル出力する
* 出力したファイルをS3の別領域にアップロードする

という要件。
S3連携部分は、[Amazon S3 Transfer Manager](https://docs.aws.amazon.com/ja_jp/sdk-for-java/latest/developer-guide/transfer-manager.html)を参考に作成しました。

```java:S3Config.java
@Bean
public S3AsyncClient s3AsyncClient() {
    return S3AsyncClient.crtBuilder()
            .region(Region.US_EAST_1)
            .credentialsProvider(DefaultCredentialsProvider.create())
            .build();
}

@Bean
public S3TransferManager TransferManager(S3AsyncClient s3Client) {
    return S3TransferManager.builder()
            .s3Client(s3Client)
            .build();
}
```

開発自体は順調で、単体テストも問題なく通っていました。

## いざ、Dockerで実行してみると…
あれ？ StackOverflowError…？
```
INFO 7 --- [         task-1] s.a.a.t.s.p.LoggingTransferListener: Transfer initiated...
WARN 1 --- [ AwsEventLoop 2] s.a.a.t.s.p.LoggingTransferListener: Transfer failed.
software.amazon.awssdk.core.exception.SdkClientException: Failed to send the request: A callback has reported failure.
	at software.amazon.awssdk.core.exception.SdkClientException$BuilderImpl.build(SdkClientException.java:111) ~[sdk-core-2.21.27.jar!/:na]
	at software.amazon.awssdk.core.exception.SdkClientException.create(SdkClientException.java:47) ~[sdk-core-2.21.27.jar!/:na]
	at software.amazon.awssdk.services.s3.internal.crt.S3CrtResponseHandlerAdapter.handleError(S3CrtResponseHandlerAdapter.java:139) ~[s3-2.21.27.jar!/:na]
	at software.amazon.awssdk.services.s3.internal.crt.S3CrtResponseHandlerAdapter.onFinished(S3CrtResponseHandlerAdapter.java:99) ~[s3-2.21.27.jar!/:na]
	at software.amazon.awssdk.crt.s3.S3MetaRequestResponseHandlerNativeAdapter.onFinished(S3MetaRequestResponseHandlerNativeAdapter.java:24) ~[aws-crt-0.28.9.jar!/:0.28.9]
Caused by: java.lang.StackOverflowError: null
```
いや、ヒープサイズも十分なはずだし、転送ファイル自体もそんなにデカくないぞ…？

# 原因切り分け
いくつか試してみて、原因の切り分けを実施。

## 駄目だったやつ

### ヒープサイズを見直す
Kubernetes環境で実行していたので、ヒープサイズを多めに取るように変更しました(Memory Limitを6Gi程度まで増やす & `-XX:MaxRAMPercentage=75`まで上げる)。が、ダメ。

### 転送ファイルの容量を下げてみる

転送ファイルを数B程度のテキストファイルに変更してみましたが、変わらず。

### S3Clientの設定値を見直す

ガイドでは、S3Clientの設定時に`minimumPartSizeInBytes`等を明示的に設定していたので、こちらのデフォルト値が低すぎるのか、と思い、以下のように変更。
```java:S3Config.java
@Bean
public S3AsyncClient s3AsyncClient() {
    return S3AsyncClient.crtBuilder()
            .region(Region.US_EAST_1)
            .credentialsProvider(DefaultCredentialsProvider.create())
            // 初期読み込みバッファサイズを指定
            .initialReadBufferSizeInBytes(256 * MB)
            // Multipart処理最小バイトを指定
            .minimumPartSizeInBytes(8 * MB)
            .build();
}
```
…変わらず。

## 部分的に成功したやつ

### Spring Bootのバージョンを下げる

直近、Spring Bootのバージョンを3.1系から3.2.0に上げていたので、ココが怪しい？と思い、3.1.6に下げてみました。
結果、コンテナを立ち上げてすぐのタイミングでは温まっていないのかエラーが出ますが、ある程度時間が経つと成功するように…

が、これで妥協してはいけない。

## 成功したやつ

結局、単体テストでは問題なく成功しているので、インフラ自体の問題なのでは？と思い、最終的にAlpine Linuxを疑いました。

というのも、ググるとAlpineはglibcと互換性のないmusl-libcを利用しているようで、これが原因で多くのアプリケーションで正常に動かない、などの事象があるようで。

### ベースイメージをAlpine -> Ubuntu Focalに変更

同じくeclipse-temurinで配布されている、Ubuntuベースの`eclipse-temrin:17-jre-focal`にベースイメージを変更しました。
すると…
```
INFO 7 --- [         task-1] s.a.a.t.s.p.LoggingTransferListener: Transfer initiated...
INFO 7 --- [       Thread-2] s.a.a.t.s.p.LoggingTransferListener: |===                 | 18.9%
INFO 7 --- [       Thread-2] s.a.a.t.s.p.LoggingTransferListener: |=======             | 37.8%
INFO 7 --- [         task-1] s.a.a.t.s.p.LoggingTransferListener: |===========         | 56.7%
INFO 7 --- [         task-1] s.a.a.t.s.p.LoggingTransferListener: |===============     | 75.6%
INFO 7 --- [         task-1] s.a.a.t.s.p.LoggingTransferListener: |==================  | 94.6%
INFO 7 --- [         task-1] s.a.a.t.s.p.LoggingTransferListener: |====================| 100.0%
INFO 7 --- [         task-1] s.a.a.t.s.p.LoggingTransferListener: Transfer complete!
```
あっさり成功ｗ

ちなみに、focalはUbuntu 20.04 LTSなんですが、一世代後のLTSであるjammy(Ubuntu 22.04)を採用した場合、Dockerバージョンが古いとビルドに失敗したりします。。。


# もう少し工夫する
とはいえ、Ubuntuベースだとコンテナイメージのサイズが大きくなってしまいます。やっぱり小さくしたい。

## Distrolessを使う

結局これになるのかなと。

shが使えなくなってしまうので、他イメージと使い勝手も違って最初は戸惑うことも多いですが、
* debugイメージがあるので、shも使おうと思えば使える
* イメージタグにnonrootを指定すると、サクッと非rootユーザーで動かすコンテナを作ることができる
* 余計な依存を埋め込まないのでセキュアにしやすい

など、慣れると結構良かったり。

### ただし…

DistrolessのJavaイメージについては、現状DeprecatedになっているOpenJDKが使われている様子。

[ソースコード](https://github.com/GoogleContainerTools/distroless/blob/main/java/BUILD)を見ると、Java 21からは我らがEclipse Temurinを採用して、徐々に11,17も移行していくようですが…

この辺が気になる人は、結構ガチらないと大変かもしれません。

## どうしてもAlpineが使いたい！

とはいえAlpineが使いたい！というとき。

https://qiita.com/dojyorin/items/5a53b6eed77803d558ed

※ 今回の事象がglibc問題によって引き起こされたかどうかまでは分かってません。

# 結論

* 用途次第だけど、よほどAlpineを使いたいというこだわりが無い限りは採用しない方が無難な気がします。
* 単体テストでOKでも、コンテナ実行でOKとは限らない…
* ヒープとか設定値を疑うのも大事だけど、どん詰まったらFrameworkやインフラのバージョンを変えたり、OSごと入れ替えてみたりも試してみよう
* その他、コンテナ環境でJavaを使うなら[jib](https://github.com/GoogleContainerTools/jib)も良さそうですね。

# 関連記事
https://qiita.com/fyui001/items/341efff0357de6742b2d