はじめに
こんにちは。バックエンドエンジニアの齋藤です。
現在PHP→Kotlinへのリアーキテクチャを進めていますが、最近実装した機能でリリース前の負荷テストを行ったところ、JVMのクラッシュが発生しました。
リアーキプロジェクトでは、ECS・Fargate環境を利用していますが、JVMがクラッシュするとタスクが終了してしまい、エラーログ(hs_err_pidxx.log)を確認できず、また高負荷環境でのみクラッシュが発生するためローカルで再現もできず困っていました。ChatGPTから「S3に保存する方法がよいらしい」という情報をもらい、クラッシュ時にS3にログを保存して、後で確認できるようにすることにしました。
ということで、今回はECS・Fargate環境でエラーログを確認した方法を紹介します。
方法
では、早速方法に移ります。
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万人を突破したアプリの開発を支えるバックエンドエンジニアを募集しています。興味のある方、ぜひお気軽にお話しましょう!
参考
Java SE トラブルシューティング・ガイド - 致命的エラー・ログ
Configuring core dumps in docker | Dmitry Danilov
【Docker】entry-point.sh の役割と使用例
Dockerfile ENTRYPOINTについて知っておきたい大事な基本 | Kinsta®