asken テックブログ

askenエンジニアが日々どんなことに取り組み、どんな「学び」を得ているか、よもやま話も織り交ぜつつ綴っていきます。 皆さまにも一緒に学びを楽しんでいただけたら幸いです!

ECS・Fargate環境で、JVMクラッシュ時のエラーログをS3に保存する

はじめに

こんにちは。バックエンドエンジニアの齋藤です。

現在PHP→Kotlinへのリアーキテクチャを進めていますが、最近実装した機能でリリース前の負荷テストを行ったところ、JVMのクラッシュが発生しました。

リアーキプロジェクトでは、ECS・Fargate環境を利用していますが、JVMがクラッシュするとタスクが終了してしまい、エラーログ(hs_err_pidxx.log)を確認できず、また高負荷環境でのみクラッシュが発生するためローカルで再現もできず困っていました。ChatGPTから「S3に保存する方法がよいらしい」という情報をもらい、クラッシュ時にS3にログを保存して、後で確認できるようにすることにしました。

ということで、今回はECS・Fargate環境でエラーログを確認した方法を紹介します。

20240731133859

方法

では、早速方法に移ります。

Dockerfileとスクリプトの設定

3つのファイルが関係します。以下の流れで呼び出しが行われます。

Dockerfile → entrypoint.sh → crash-handler.sh (今回新規作成)

1. Dockerfileの更新

Dockerfileに必要なパッケージのインストールとスクリプトを追加しました。

以下は追記した部分です。awscliはS3にファイルをアップする時に使います。

# 必要なパッケージのインストール
RUN apt-get update && apt-get install -y --no-install-recommends awscli

# エントリーポイントスクリプトのコピー
COPY entrypoint.sh /entrypoint.sh
COPY crash-handler.sh /crash-handler.sh

# Docker内にファイルをコピーし、/entrypoint.shからcrash-handler.shを実行できるようにする
COPY . /app
RUN chmod +x /entrypoint.sh /app/crash-handler.sh

2. entrypoint.shの作成

exec "$@" 以降の部分に、以下を追記して、プロセスの終了時にクラッシュハンドラを呼び出すようにしました。

# アプリケーションをバックグラウンドで実行
exec "$@" &

# アプリケーションプロセスID(バックグラウンドコマンドのプロセスID)を取得
PID=$!

# 'wait $PID' のステータスが0でない場合(異常終了時)に、クラッシュハンドラーを実行
wait $PID
if [ $? -ne 0 ]; then
  /app/crash-handler.sh $PID
fi
  • (補足1) entrypoint.sh : ENTRYPOINT は、Dockerのコンテナの起動時に実行されるコマンドやスクリプトを指定するもの。entrypoint.shはそれをシェル化したもの。
  • (補足2) exec "$@" はシェルの引数として渡されたコマンドを実行する。 & は、直前のコマンドをバックグラウンドで実行する。
  • (補足3) $! はバックグラウンドコマンドのプロセスIDを取得する変数。
  • (補足4) $? は、シェルが直前に実行したコマンドの終了ステータスを保持する。
  • (補足5) -ne は not equalを表す。

3. crash-handler.shの作成

このファイルは新規作成しました。エラーファイルが存在した場合に、S3にアップロードする処理です。Dockerfileと同じ階層に置きました。

#!/bin/bash
PID=$1
BUCKET_NAME="バケット名"

sleep 30

# エラーファイルの確認
ERROR_FILE=$(ls /hs_err*)

if [ -n "$ERROR_FILE" ]; then
  # エラーファイルをS3にアップロード
  aws s3 cp $ERROR_FILE s3://$BUCKET_NAME/jvm_error/
fi

ファイルが生成されることの確認

  • ターミナルにAWS CLIの認証情報を貼り付けます。
  • ターミナルに以下を貼り付けます。タスクのarnは、タスクのページに記載があります。
aws ecs execute-command \
--cluster <クラスター名> \
--task <タスクのarn> \
--container <コンテナ名> \
--interactive \
--command "/bin/sh"
  • 以下コマンドを実行し、対象のプロセスの確認をします。

apt-get update && apt-get install -y procps

ps aux

# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   2576   924 pts/0    Ss+  08:31   0:00 /bin/sh /entryp
root        28  4.1 10.8 20926496 3499712 pts/0 Sl+ 08:31   0:57 /usr/bin/java -
root       141  0.0  0.0   2576   936 pts/1    Ss+  08:46   0:00 /bin/sh
root       362  0.0  0.0   8480  4180 pts/2    R+   08:55   0:00 ps aux
  • 上記の場合、javaのプロセスが28なので、以下を実行して、SIGSEGVのシグナルを送ります。-11 は、SIGSEGV のIDです。

kill -11 28

  • S3の指定されたバケットに、エラーログファイルが保存されます。

まとめ

上記の方法で、無事にクラッシュが発生している部分を特定することができました。もし同様の状況の方がいれば、参考になりましたら幸いです。

askenでは、今年ユーザー1000万人を突破したアプリの開発を支えるバックエンドエンジニアを募集しています。興味のある方、ぜひお気軽にお話しましょう!

hrmos.co

参考

Java SE トラブルシューティング・ガイド - 致命的エラー・ログ

Configuring core dumps in docker | Dmitry Danilov

【Docker】entry-point.sh の役割と使用例

Dockerfile ENTRYPOINTについて知っておきたい大事な基本 | Kinsta®

bashでのPID取得方法まとめ($$、$PPID、$!、$BASHPID) #Bash - Qiita

Shell 特殊変数 #shell - Qiita