😊

inotifyでファイルの変更を監視するサイドカーを作る

2023/01/15に公開

コンテナでログなどでファイルが出力された際に、そのままだとコンテナが停止してそのまま消えてしまうため、外部のストレージに出力したいなどあるかと思います。
通常のログであればFluentBitなどで送るなり、そもそも標準出力をするなりすれば良いかと思いますが、今回はJavaでOutOfMemoryErrorが発生した際に出力されるバイナリ形式のヒープダンプを出力するという内容です。

サイドカーを起動してinotifyでファイルの作成を監視しファイル出力する処理を書くことで、サイドカー経由で外部ストレージに出力することが実現出来ます。

inotifyを利用したイメージの作成

一番重要なコンテナイメージの作成です。

FROM alpine:3.17.0 as builder

RUN apk update
RUN apk add inotify-tools

RUN mkdir -p /var/log/jvmlogs

COPY main.sh /bin/main.sh
RUN chmod +x /bin/main.sh

FROM builder
CMD /bin/main.sh
main.sh
#!/bin/sh

LOGS_DIR=/var/log/jvmlogs

inotifywait -mq -r -e create ${LOGS_DIR} |
  while read path action file; do
      # ここでaws s3などで外部ストレージに出力する処理を書く。今回は標準出力のみ
      echo "$(date) ${path}${file} が更新されました"
  done

このシェルでは /var/log/jvmlogs のディレクトリを監視して、ファイルが作成されたらメッセージを出力しています。
このディレクトリにはアプリケーション側でファイルを出力します。
アプリケーション側のコンテナとinotifyのコンテナでボリュームマウントすることでファイルの監視が出来ます。

アプリケーション側のイメージの作成

まずOOMEが発生するコードの作成です。

ルートのエンドポイントにアクセスすると100MBの配列を生成しリストに保持するクラスを作成します。

@RestController
@RequestMapping("/")
public class HelloController {
  private List<byte[]> list = new ArrayList<>();

  @GetMapping
  public String index() {
    list.add(new byte[1024 * 1024 * 100]); // 100MBの配列を生成する
    return "hello";
  }
}

Dockerfileです。

FROM openjdk:17-jdk-alpine3.14
RUN mkdir -p /var/log/jvmlogs
ARG JAR_FILE=build/libs/oome-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar

ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]

docker-compose

最後にdocker-compose.yamlです。

version: "3.7"

services:
  app:
    build:
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - "JAVA_OPTS=-Xmx512m -XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/jvmlogs/"
    volumes:
      - type: volume
        source: jvmlogs_volume
        target: /var/log/jvmlogs/
  inotify:
    build:
      context: inotify
      dockerfile: Dockerfile
    volumes:
      - type: volume
        source: jvmlogs_volume
        target: /var/log/jvmlogs/

volumes:
  jvmlogs_volume:

Javaの起動オプションには最大ヒープサイズに512mbを指定し、OutOfMemoryErrorが発生した際にコンテナを落として、/var/log/jvmlogs/にヒープダンプを出力する設定を入れました。

また/var/log/jvmlogs/をappコンテナとinotifyコンテナでボリュームマウントする設定を入れています。
これによってディレクトリの共有が出来ました。

テスト

docker-compose up -dを実行してコンテナを起動します。

curl "http://localhost:8080/"を5回ほど実行します。

アプリケーション側のログを確認します。

$ docker logs spring-oome-app-1 
・・・途中省略・・・
2023-01-15T06:55:05.093Z  INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /var/log/jvmlogs/java_pid1.hprof ...
Heap dump file created [440052960 bytes in 0.885 secs]
Terminating due to java.lang.OutOfMemoryError: Java heap space

想定通りOOMEが発生し/var/log/jvmlogs/java_pid1.hprofが出力されています。

inotify側のログを確認します。

$ docker logs spring-oome-inotify-1
Sun Jan 15 06:55:09 UTC 2023 /var/log/jvmlogs/java_pid1.hprof が更新されました

無事ファイルの作成を検知してメッセージを出力することが出来ました!

ECSで上記を実現してs3に吐き出す方法はまた機会があれば作成したいと思います。

Discussion